我們現在知道了每個函數的this是在運行的時候進行綁定的,完全取決于函數的調用位置,也就是該函數的調用方法。
關于調用位置
在理解this的綁定過程之前,我們了解一下調用位置,調用位置表示的是函數所被調用的位置,而不是其聲明的位置。
如何知道函數的調用位置,最重要的是分析函數的調用棧(即為了到達當前執行位置所調用的所有函數)。那么調用位置就是當前正在執行函數的前一個調用中。
function baz() {
//當前的調用棧是baz
//當前的調用位置是全局作用域,即當前調用棧的前一個調用
console.log('baz');
bar();
}
function bar() {
//當前調用棧是 baz-->bar
//當前的調用位置是:baz
console.log('bar');
foo();
}
function foo() {
//當前的調用棧是baz --> bar --> foo
//當前調用位置是bar
}
baz(); //<-- baz的調用位置就是全局作用域
注意如上,我們是如何分析調用棧和調用位置的,因此這樣決定了this的綁定。
關于綁定規則
接下來我們看下在函數的執行過程中,調用位置如何決定this的綁定對象。
-
默認綁定規則
最常見的函數調用類型是獨立函數調用,如下:
function foo() {
console.log(this.a);
}
var a = 2;
foo(); //2
在上面的代碼中,我們看到了輸出的值為2,我們知道在全局作用域中聲明的變量會變成是全局對象的一個屬性。我們在調用foo()的時候,函數調用應用了this的默認綁定,因此this指向的是的全局對象。
為什么說是默認綁定呢?因為foo()的調用是不帶有任何修飾的函數引用進行調用的,因此只能是使用默認綁定規則。
-
隱式綁定
另外一種需要考慮的規則是調用位置是否有上下文對象,或者說是否被某個對象擁有或者包含。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo()
}
obj.foo(); //2
看上面的代碼,注意無論直接在obj中定義foo函數還是先定義了foo函數,然后添加為obj的引用,這個函數嚴格來說都不屬于obj對象。然而,調用位置會使用obj上下文來引用函數。
當函數引用有上下文對象時,隱式綁定會把函數調用中的this綁定到這個上下文對象中。所以上面的this.a和obj.a是一樣的。
有一點值得注意的就是隱式丟失,就是被隱式綁定的函數會丟失其綁定對象而會應用默認綁定,從而將this綁定到全局作用域或者undefined上,這個要取決于是否是嚴格模式。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo;
var a = 'oops global';
bar(); //'oops global
如上,bar是obj.foo的一個引用,但是實際上其引用的是foo的本身,因此此時的bar是不帶有任何修飾符的函數調用,因此引用應用了默認綁定。
function foo() {
console.log(this.a);
}
function doFoo(fn) {
//其實引用的是foo
fn(); //<--調用位置
}
var obj = {
a: 2,
foo: foo
}
var a = 'oops global';
doFoo(obj.foo); //'oops global
參數傳遞其實就是一種隱式賦值,因此我們傳入函數時也會被隱式的賦值。
看到上面的例子,可以發現在日常使用回調函數丟失this的綁定是非常常見的。
-
顯示綁定
前面的隱式綁定我們可以看到,函數的調用是通過一個對象內部的一個屬性進行引用的,從而將this綁定到這個對象上。
這里我們先介紹兩個方法apply()和call(),他們的第一個參數是一個對象,這個是給this準備的,這樣在調用的時候可以將其綁定到this上。
function foo() {
console.log(this.a);
}
var obj = {
a: 2
}
foo.call(obj); //2
這個應該是比較好理解的,在通過call調用的時候,強制的把它的this綁定到obj上。
然而顯示綁定仍然無法解決this丟失的問題。
function foo() {
console.log(this.a);
}
var obj = {
a: 2
}
var bar = function() {
foo.call(obj);
}
bar(); //2
setTimeout(bar, 200); //2
//硬綁定的bar不可以再修改它的this
bar.call(window); //2
如上我們用個函數包裹住foo,在函數體內部調用foo,并顯示綁定了this到obj對象上,那么后面無論如何調用函數bar,但是foo的調用始終都是在obj上調用,這就是一種顯示的硬綁定。
我們常用的Function.prototype.bind就是一種很好的內置方法。硬綁定的場景就是創建一個包裹函數,負責接受參數并返回值:
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2
}
var bar = function() {
return foo.apply(obj, arguments);
}
var b = bar(3); // 2 3
console.log(b); //5
另一種方法就是創建一個可以重復使用的輔助函數:
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
在ES5提供的bind方法中會返回一個硬編碼的新函數,它會把你指定的參數設置為this的上下文并調用原始函數。
-API調用的上下文,你可以看到很多第三方庫和JavaScript語言和宿主環境中許多新的內置函數,都提供了一個可選的參數,通常叫做'上下文'(context),它的作用和bind一些樣,確保你的回調函數使用指定的this。如下:
function foo(el) {
console.log(el, this.id);
}
var obj = {
id: 'awesome'
}
//調用foo時把this綁定到obj
[1,2,3].forEach(foo, obj);
//這些函數實際上就是通過call或者apply實現了顯示的綁定。
-
new綁定
在JavaScript中平常所聲明的一些函數其實也就是我們常說的構造函數,沒有什么特別的區別。因為在JavaScript中所有的函數都可以通過new操作符進行調用,實際上并不存在什么所謂的構造函數,只有對函數的構造調用。
使用new操作符來調用函數的時候,會有下面的操作:
1. 創建一個全新的對象;
2. 這個新對象會被執行[[Prototype]]連接。
3. 這個新對象會綁定到函數調用的this。
4. 如果函數沒有返回其它對象,那么new表達式中的函數調用會自動返回這個全新的對象。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); //2
如上,使用new調用foo的時候,我們會創造一個新對象并把它綁定到foo調用中的this上。
小結
以上四種this的綁定規則我們已經介紹過了,我們要做的就是找到函數的調用位置并應用對應的調用規則就可以了。
綁定例外
如果將null或者是undefined作為this的綁定對象傳入call、apply、bind,這些值在調用的時候會被忽略,實際上應用的是默認綁定規則。
function foo() {
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
一般常見的做法是使用apply(...)來'展開'一個數組,并當做參數傳入一個函數。類似的,bind(...)可以對參數進行柯里化(預先設置一些參數),這種方法有時候非常有用。
function foo() {
console.log("a:" + a, "b"+ b);
}
//把數組展開成參數
foo.apply(null, [2, 3]); //a: 2, b: 3
//使用bind進行柯里化
var bar = foo.bind(null, 2);
bar(3); //a: 2, b:3
更安全的this
更安全的this是傳入一個空對象,把this綁定到這個對象上,不會對你的代碼產生任何的副作用。
在JavaScript中創建一個空對的方法是Object.create(null)。該方法和{}很像,但是不會創建Object.prototype這個委托,所以可以說比{}更空。
function foo(a, b){
console.log('a:'+a, 'b:'+b);
}
//創建個空對象
var ? = Object.create(null);
//把數組展開成參數
foo.apply(?, [2,3]); //a: 2, b:3
//使用bind進行柯里化
var bar = foo.bind(?, 2);
bar(3); //a:2, b:3
間接引用
有的時候可能會創建一個函數的間接引用,那么在這種情況下,調用這個函數會應用默認規則。這種情況容易發生在賦值時候。
function foo() {
console.log(this.a);
}
var a = 2;
var o = {a:3, foo: foo};
var p = {a: 4};
o.foo() // 3
p.foo = o.foo;
p.foo(); //2
上面的賦值表達式返回的是目標函數的引用,因此調用位置是foo()而不是p.foo()或者o.foo()。
this詞法
在ES6中,我們使用了更為簡便的方法來實現函數,那就是箭頭函數,它是根據外層函數或者全局作用域來實現this的綁定。
function foo() {
return (a) => {
console.log(this.a); //this繼承自foo()
}
}
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,箭頭函數的綁定無法被修改。