this是在執行時繫結的,並不是在編寫時繫結,它的上下文取決於函式呼叫時的各種條件。this的繫結和函式宣告的位置沒有任何關係,只取決於函式的呼叫方式。 當一個函式被呼叫時,會建立一個活動記錄(有時也稱為執行上下文)。這個記錄會包含函式在哪裡被呼叫(呼叫棧)。函式的呼叫方式、傳入的引數等資訊。this就是這個記錄的一個屬性,會在函式執行的過程中用到。
繫結規則
預設繫結
獨立函式呼叫,無法應用其它規則時的預設規則。
如果使用嚴格模式,則不能將全域性物件用於預設繫結,因此this會繫結到undefined。
隱式繫結
呼叫位置是否有上下文物件,或者說是否被某個物件擁有或者包含。如下所示,呼叫位置會使用obj上下文引用函式,因此可以說函式被呼叫時obj物件“擁有”或者“包含”它。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
複製程式碼
這種情況同樣適用於foo存在於obj的原型鏈上,如下所示:
function foo() {
console.log(this.a);
}
var fo = {
foo: foo
};
// 建立一個物件obj,使之原型指向fo
var obj = Object.create(fo);
obj.a = 2;
obj.foo(); // 2
複製程式碼
物件屬性引用鏈中只有上一層或者最後一層在呼叫位置中起作用,如下
function foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
複製程式碼
隱式丟失
一個最常見的this繫結問題就是被
隱式繫結
的函式會丟失繫結物件,也就是說它會應用預設繫結
。
思考下面的程式碼:
function foo() {
console.log(this.a);
}
var obj = {
a: 42,
foo: foo
};
var bar = obj.foo;
var a = "oops";
bar(); // oops
複製程式碼
雖然bar是obj.foo的一個引用,但實際上,它引用的是foo函式本身,因此此時的bar()其實是一個不帶任何修飾的函式呼叫,因此應用了預設繫結。
一種更出乎意料的情況發生在傳入回撥函式時:
function foo() {
console.log(this.a);
}
function doFoo(fn) {
fn();
}
var obj = {
a: 2,
foo: foo
};
var a = "oops";
doFoo(obj.foo); // oops
複製程式碼
引數傳遞其實是一種隱式傳遞,因此我們傳入函式時也會被隱式賦值,所以結果和上個例子一樣。
顯示繫結
隱式繫結
時,必須在一個物件內部包含一個指向函式的屬性,並通過這個屬性間接引用函式,從而把this間接繫結到這個物件上。如果我們不想在物件內部包含函式引用,而想在某個物件上強制呼叫函式,該怎麼辦呢?答案就是call
或apply
!
這兩個方法第一個引數是一個物件,是給this準備的,接著在呼叫函式時將其繫結到this。因為你可以直接指定this的繫結物件,因此稱之為顯示繫結
。
呼叫call
或apply
時,如果你傳入了一個原始值(字串、布林、數字)來當做this的繫結物件,這個原始值會轉化為它的物件形式。這通常被稱為“裝箱”。
可以重複使用的輔助函式
function foo(something) {
return this.a + somthing;
}
// 簡單的輔助繫結函式
function bind(fn, obj) {
return function() {
return fn.apply(obj, arguments);
};
}
var obj = {a: 2};
var bar = bind(foo, obj);
bar(3); // 5
複製程式碼
由於這是一種非常常用的模式,因此在Es5中提供了內建的方法Function.prototype.bind
。
第三方庫的許多函式,以及JavaScript語言中許多新的內建函式,都提供了一個可選的引數,通常被稱為“上下文”,其作用和bind一樣,確保你的回撥函式可以使用指定的this。
function foo(num) {
console.log(num, this.a);
}
var obj = {
a: 2
};
[1,2,3].forEach(foo, obj);
複製程式碼
這些函式實際上就是通過call或者apply實現了顯示繫結,這樣你可以少些一些程式碼。
new繫結
使用new
來呼叫函式,會執行下面的操作:
- 建立一個全新的物件。
- 這個新物件會被執行[[Prototype]]連線。
- 這個新物件會被繫結到函式呼叫的this。
- 如果函式沒有返回其它物件。那麼new表示式中的函式呼叫會自動返回這個物件。
思考下面程式碼:
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
複製程式碼
使用new
來呼叫foo時,會建立一個新物件,並繫結到foo()呼叫中的this上。
優先順序
- new繫結
- 顯示繫結
- 隱式繫結
- 預設繫結
箭頭函式
箭頭函式不使用this的四種標準規則,而是根據外層作用域來決定this。
function foo() {
return (a) => {
console.log(this.a);
};
}
var obj1 = {a: 2};
var obj2 = {a: 3};
var bar = foo.call(obj1);
bar.call(obj2); // 2, 不是3
複製程式碼
foo()內部建立的箭頭函式會捕獲呼叫時foo()的this。由於foo()的this繫結到obj1,bar的this也會繫結到obj1,箭頭函式的繫結無法被修改。(new也不行!)