為什麼要用this
回答這個問題我們就先看看如果不使用this
會出現什麼問題。試想下面程式碼如果不使用this
應該怎麼寫:
function speak(){
var name = this.name
console.log(`Hello I am ${name}`)
}
var me = {
name: 'a',
speak: speak
}
var you = {
name: 'b',
speak: speak
}
me.speak() //Hello I am a
you.speak() //Hello I am b
複製程式碼
this
可以在同一個執行環境中使用不同的上下文物件。它其實提供了一種更加優雅的方式來隱式“傳遞”一個物件引用,因此可以使API設計的更加簡潔且易於複用。
this到底是誰
this
既不是自身也不是當前函式的作用域。我們可以通過程式碼來測試。
- 判斷是不是自身
function fn(){
console.log(this.name)
}
fn.name = 'xxx'
fn() //undefined
複製程式碼
- 判斷是不是作用域
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); //undefined
複製程式碼
那麼this
到底是誰呢?其實不一定,this
是執行時繫結的,所以取決於函式的執行上下文。
當一個函式被呼叫時,會建立一個活動記錄(執行上下文)。這個記錄會包含函式在哪裡被呼叫(呼叫棧)、函式的呼叫方法、傳入的引數等資訊,this也是這裡的一個屬性。
如何判斷this指向
確定this
指向就是確定函式的執行上下文,也就是“誰呼叫的它”,有以下幾種判斷方式:
獨立函式呼叫
function foo(){
console.log(this.a)
}
var a = 2
foo() // 2
複製程式碼
這種直接呼叫的方式this
指向全域性物件,如果是在瀏覽器就指向window
物件上下文(隱式繫結)
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
複製程式碼
foo
雖然被定義在全域性作用域,但是呼叫的時候是通過obj
上下文引用的,可以理解為在foo
呼叫的那一刻它被obj
物件擁有。所以this
指向obj
。
這裡有兩個問題:
- 鏈式呼叫
鏈式呼叫的情況下只有最後一層才會影響呼叫位置,例如:
obj1.obj2.obj3.fn() //這裡的fn中的this指向obj3
複製程式碼
- 引式丟失
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函式別名!
var a = "xxxxx"
bar(); // xxxxx
複製程式碼
這裡的bar
其實是引用了obj.foo
的地址,這個地址指向的是一個函式,也就是說bar
的呼叫其實符合“獨立函式呼叫”規則。所以它的this
不是obj
。
回撥函式其實就是隱式丟失
稍微改一下上面的程式碼:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "xxxxx"
setTimeout( obj.foo ,100); // xxxxx
複製程式碼
我們看到,回撥函式雖然是通過obj
引用的,但是this
也不是obj
了。其實內建的setTimeout()函式實現和下面的虛擬碼類似:
function setTimeout(fn, delay){
//等待delay毫秒
fn()
}
複製程式碼
其實這段程式碼隱藏這一個操作就是fn=obj.foo
,這和上面例子中的bar=obj.foo
異曲同工。
顯式繫結
顯式繫結的說法是和隱式繫結相對的,指的是通過call
、apply
、bind
顯式地更改this
指向。
這三個方法第一個引數是this
要指向的物件。
注意,如果你給第一個引數傳遞一個值(字串、布林、數字)型別的話,這個值會被轉換成物件形式(呼叫new String(..)、new Boolean(..)、new Number(..))。
這三個方法中的bind
方法比較特殊,它可以延遲方法的執行,這可以讓我們寫出更加靈活的程式碼。它的原理也很容易模擬:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
function bind(fn, obj) {
return function() {
return fn.apply( obj, arguments );
};
}
var obj = {
a:2
};
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
複製程式碼
注意:如果第一個引數傳入null或者undefined,這個值會被忽略,相當於符合獨立函式呼叫規則
new繫結
Js中new
與傳統的面向類的語言機制不同,Js中的“建構函式”其實和普通函式沒有任何區別。
其實當我們使用new
來呼叫函式的時候,發生了下列事情:
- 建立一個全新的物件
- 這個新物件會被執行“原型”連結
- 這個新物件會被繫結到呼叫的this
- 如果函式沒有物件型別的返回值,這個物件會被返回
其中,第三步繫結了this
,所以“建構函式”和原型中的this
永遠指向new
出來的例項。
優先順序
以上四條判斷規則的權重是遞增的。判斷的順序為:
- 函式是
new
出來的,this
指向例項 - 函式通過call、apply、bind繫結過,
this
指向繫結的第一個引數 - 函式在某個上下文物件中呼叫(隱式繫結),
this
指向上下文物件 - 以上都不是,
this
指向全域性物件
嚴格模式下的差異
以上所說的都是在非嚴格模式下成立,嚴格模式下的this
指向是有差異的。
- 獨立函式呼叫:
this
指向undefined
- 物件上的方法:
this
永遠指向該物件 - 其他沒有區別
箭頭函式中的this
箭頭函式不是通過function
關鍵字定義的,也不使用上面的this
規則,而是“繼承”外層作用域中的this
指向。
其實一起雖然沒有箭頭函式,我們也經常做和箭頭函式一樣效果的事情,比如說:
function foo() {
var self = this;
setTimeout( function(){
console.log( self );
}, 100 );
}
複製程式碼
getter與setter中的this
es6中的getter
或setter
函式都會把this
繫結到設定或獲取屬性的物件上。
function sum() {
return this.a + this.b + this.c;
}
var o = {
a: 1,
b: 2,
c: 3,
get average() {
return (this.a + this.b + this.c) / 3;
}
};
Object.defineProperty(o, 'sum', { get: sum, enumerable: true, configurable: true} );
console.log(o.average, o.sum); // logs 2, 6
複製程式碼
參考資料
MDN - this
《你不知道的javascript 上卷》