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