難道我們就不能徹底搞清楚“this”嗎?在某種程度上,幾乎所有的 JavaScript 開發(fā)人員都曾經(jīng)思考過“this”這個事情。對我來說,每當“this”出來搗亂的時候,我就會想方設法地去解決掉它,但過后就把它忘了,我想你應該也曾遇到過類似的場景。但是今天,讓我們弄明白它,讓我們一次性地徹底解決“this”的問題,一勞永逸。
前幾天,我在圖書館遇到了一個意想不到的事情。
這本書的整個第二章都是關于“this”的,我很有自信地通讀了一遍,但是發(fā)現(xiàn)其中有些地方講到的“this”,我居然搞不懂它們是什么,需要去猜測。真的是時候反省一下我過度自信的愚蠢行為了。我再次把這一章重讀了好幾遍,發(fā)覺這里面的內(nèi)容是每個 Javascript 開發(fā)人員都應該了解的。
因此,我嘗試著用一種更徹底的方式和更多的示例代碼來展示 凱爾·辛普森 在他的這本書 你不知道的 Javascript 中描述的那些規(guī)范。
在這里我不會通篇只講理論,我會直接以曾經(jīng)困擾過我的困難問題為例開始講起,我希望它們也是你感到困難的問題。但不管這些問題是否會困撓你,我都會給出解釋說明,我會一個接一個地向你介紹所有的規(guī)則,當然還會有一些追加內(nèi)容。
在開始之前,我假設你已經(jīng)了解了一些 JavaScript 的背景知識,當我講到 global、window、this、prototype 等等的時候,你知道它們是什么意思。這篇文章中,我會同時使用 global 和 window,在這里它們就是一回事,是可以互換的。
在下面給出的所有代碼示例中,你的任務就是猜一下控制臺輸出的結(jié)果是什么。如果你猜對了,就給你自己加一分。準備好了嗎?讓我們開始吧。
Example #1
function foo() {?
console.log(this);?
bar();?
}
function bar() {?
console.log(this);?
baz();?
}
function baz() {?
console.log(this);?
}
foo();
復制代碼你被難住了嗎?為了測試,你當然可以把這段代碼復制下來,然后在瀏覽器或者 Node 的運行環(huán)境中去運行看看結(jié)果。再來一次,你被難住了嗎?好吧,我就不再問了。但說真的,如果你沒被難住,那就給你自己加一分。
如果你運行上面的代碼,就會在控制臺中看到 global 對象被打印出三次。為了解釋這一點,讓我來介紹 第一個規(guī)則,默認綁定。規(guī)則規(guī)定,當一個函數(shù)執(zhí)行獨立調(diào)用時,例如只是 funcName();,這時函數(shù)的“this”被指向 global 對象。
需要理解的是,在調(diào)用函數(shù)之前,“this”并沒有綁定到這個函數(shù),因此,要找到“this”,你應該密切注意該函數(shù)是如何調(diào)用,而不是在哪里調(diào)用。所有三個函數(shù) foo();bar(); 和 baz();_ 都是獨立的調(diào)用,因此這三個函數(shù)的“this”都指向全局對象。
Example #2
‘use strict’;
function foo() {
console.log(this);
bar();
}
function bar() {
console.log(this);
baz();
}
function baz() {
console.log(this);
}
foo();
復制代碼注意下最開始的“use strict”。在這種情況下,你覺得控制臺會打印什么?當然,如果你了解 strict mode,你就會知道在嚴格模式下 global 對象不會被默認綁定。所以,你得到的打印是三次 undefined 的輸出,而不再是 global。
回顧一下,在一個簡單調(diào)用函數(shù)中,比如獨立調(diào)用中,“this”在非嚴格模式下指向 global 對象,但在嚴格模式下不允許 global 對象默認綁定,因此這些函數(shù)中的“this”是 undefined。
為了使我們對默認綁定概念理解得更加具體,這里有一些示例。
Example #3
function foo() {
function bar() {
? console.log(this);
}
bar();
}
foo();
復制代碼foo 先被調(diào)用,然后又調(diào)用 bar,bar 將“this”打印到控制臺中。這里的技巧是看看函數(shù)是如何被調(diào)用的。foo 和 bar 都被單獨調(diào)用,因此,他們內(nèi)部的“this”都是指向 global 對象。但是由于 bar 是唯一執(zhí)行打印的函數(shù),所以我們看到 global 對象在控制臺中輸出了一次。
我希望你沒有回答 foo 或 bar。有沒有?
我們已經(jīng)了解了默認綁定。讓我們再做一個簡單的測試。在下面的示例中,控制臺輸出什么?
Example #4
var a = 1;
function foo() {?
console.log(this.a);?
}
foo();
復制代碼輸出結(jié)果是 undefined?是 1?還是什么?
如果你已經(jīng)很好地理解了之前講解的內(nèi)容,那么你應該知道控制臺輸出的是“1”。為什么?首先,默認綁定作用于函數(shù) foo。因此 foo 中的“this”指向 global 對象,并且 a 被聲明為 global 變量,這就意味著 a 是 global 對象的屬性(也稱之為全局對象污染),因此 this.a 和 var a 就是同一個東西。
隨著本文的深入,我們將會繼續(xù)研究默認綁定,但是現(xiàn)在是時候向你介紹下一個規(guī)則了。
Example #5
var obj = {?
a: 1,?
foo: function() {?
? console.log(this);?
}?
};
obj.foo();
復制代碼這里應該沒有什么疑問,對象“obj”會被輸出在控制臺中。你在這里看到的是 隱式綁定。規(guī)則規(guī)定,當一個函數(shù)被作為一個對象方法被調(diào)用時,那么它內(nèi)部的“this”應該指向這個對象。如果函數(shù)調(diào)用前面有多個對象( obj1.obj2.func() ),那么函數(shù)之前的最后一個對象(obj3)會被綁定。
需要注意的一點是函數(shù)調(diào)用必須有效,那也就是說當你調(diào)用 obj.func() 時,必須確保 func 是對象 obj 的屬性。
因此,在上面的例子中調(diào)用 obj.foo() 時,“this”就指向 obj,因此 obj 被打印輸出在控制臺中。
Example #6
function logThis() {?
console.log(this);?
}
var myObject = {?
a: 1,?
logThis: logThis?
};
logThis();?
myObject.logThis();
復制代碼你被難住了?我希望沒有。
跟在 myObject 后面的這個全局調(diào)用 logThis() 通過 console.log(this) 打印的是 global 對象;而 myObject.logThis() 打印的是 myObject 對象。
這里需要注意一件有趣的事情:
console.log(logThis === myObject.logThis); // true
復制代碼為什么不呢?它們當然是相同的函數(shù),但是你可以看到 如何調(diào)用_logThis_ 會讓其中的“this”發(fā)生改變。當 logThis 被單獨調(diào)用時,使用默認綁定規(guī)則,但是當 logThis 作為前面的對象屬性被調(diào)用時,使用隱式綁定規(guī)則。
不管采用哪條規(guī)則,讓我們看看是怎么處理的(雙關語)。
Example #8
function foo() {?
var a = 2;?
this.bar();?
}
function bar() {?
console.log(this.a);?
}
foo();
復制代碼控制臺輸出什么?首先,你可能會問我們可以調(diào)用“_this.bar()”嗎?當然可以,它不會導致錯誤。
就像示例 #4 中的 var a 一樣,bar 也是全局對象的屬性。因為 foo 被單獨調(diào)用了,它內(nèi)部的“this”就是全局對象(默認綁定規(guī)則)。因此 foo 內(nèi)部的 this.bar 就是 bar。但實際的問題是,控制臺中輸出什么?
如果你猜的沒錯,“undefined”會被打印出來。
注意 bar 是如何被調(diào)用的?看起來,隱式綁定在這里發(fā)揮作用。隱式綁定意味著 bar 中的“this”是其前面的對象引用。bar 前面的對象引用是全局對象,在 foo 里面是全局對象,對不對?因此在 bar 中嘗試訪問 this.a 等同于訪問 [global object].a。沒有什么意外,因此控制臺會輸出 undefined。
太棒了!繼續(xù)向下講解。
Example #7
var obj = {?
a: 1,?
foo: function(fn) {?
? console.log(this);?
? fn();?
}?
};
obj.foo(function() {?
console.log(this);?
});
復制代碼請不要讓我失望。
函數(shù) foo 接受一個回調(diào)函數(shù)作為參數(shù)。我們所做的就是在調(diào)用 foo 的時候在參數(shù)里面放了一個函數(shù)。
obj.foo( function() { console.log(this); } );
復制代碼但是請注意 foo 是 如何 被調(diào)用的。它是一個單獨調(diào)用嗎?當然不是,因此第一個輸出到控制臺的是對象 obj 。我們傳入的回調(diào)函數(shù)是什么?在 foo 內(nèi)部,回調(diào)函數(shù)變?yōu)?fn ,注意 fn 是 如何 被調(diào)用的。對,因此 fn 中的“this”是全局對象,因此第二個被輸出到控制臺的是全局對象。
希望你不會覺得無聊。順便問一下,你的分數(shù)怎么樣?還可以嗎?好吧,這次我準備難倒你了。
Example #8
var arr = [1, 2, 3, 4];
Array.prototype.myCustomFunc = function() {
console.log(this);
};
arr.myCustomFunc();
復制代碼如果你還不知道 Javascript 里面的 .prototype 是什么,那你就權且把它和其他對象等同看待,但如果你是 JavaScript 開發(fā)者,你應該知道。你知道嗎?努努力,再去多讀一些關于原型鏈相關的書籍吧。我在這里等著你。
那么打印輸出的是什么?是 Array.prototype 對象?錯了!
這是和之前相同的技巧,請檢查 custommyfunc 是 如何 被調(diào)用的。沒錯,隱式綁定把 arr 綁定到 myCustomFunc,因此輸出到控制臺的是 arr[1,2,3,4]。
我說的,你理解了嗎?
Example #9
var arr = [1, 2, 3, 4];
arr.forEach(function() {?
console.log(this);?
});
復制代碼執(zhí)行上述代碼的結(jié)果是,在控制臺中輸出了 4 次全局對象。如果你錯了,也沒關系。請再看示例#7。還沒理解?下一個示例會有所幫助。
Example #10
var arr = [1, 2, 3, 4];
Array.prototype.myCustomFunc = function(fn) {?
console.log(this);?
fn();?
};
arr.myCustomFunc(function() {?
console.log(this);?
});
復制代碼就像示例 #7 一樣,我們將回調(diào)函數(shù) fn 作為參數(shù)傳遞給函數(shù) myCustomFunc。結(jié)果是傳入的函數(shù)會被獨立調(diào)用。這就是為什么在前面的示例(#9)中輸出全局對象,因為在 forEach 中傳入的回調(diào)函數(shù)被獨立調(diào)用。
類似地,在本例中,首先輸出到控制臺的是 arr,然后是輸出的是全局對象。我知道這看上去有點復雜,但我相信如果你能再多用點心,你會弄明白的。
讓我們繼續(xù)使用這個數(shù)組的示例來介紹更多的概念。我想我會在這里使用一個簡稱,WGL 怎么樣?作為 WHAT.GETS.LOGGED 的簡稱?好吧,在我開始老生常談之前,下面是另外一個例子。
Example #11
var arr = [1, 2, 3, 4];
Array.prototype.myCustomFunc = function() {?
console.log(this);
(function() {?
console.log(this);?
})();
};
arr.myCustomFunc();
復制代碼那么,輸出是?
答案和示例 #10 完全一樣。輪到你了,說一說為什么首先輸出的是 arr?你看到第一個 console.log(this) 的下面有一段復雜的代碼,它被稱為 IIFE(立即調(diào)用的函數(shù)表達式)。這個名字不用再過多解釋了,對吧?被 (…)(); 這樣形式封裝的函數(shù)會立即被調(diào)用,也就是說等同于被獨立調(diào)用,因此它內(nèi)部的“this”是全局變量,所以輸出的是全局變量。
要來新概念了!讓我們看看你對 ES2015 的熟悉程度。
Example #12
var arr = [1, 2, 3, 4];
Array.prototype.myCustomFunc = function() {?
console.log(this);
(function() {?
? console.log(‘Normal this : ‘, this);?
})();
(() =\> {?
? console.log(‘Arrow function this : ‘, this);?
})();
};
arr.myCustomFunc();
復制代碼除了 IIFE 后面的增加了 3 行代碼之外,其他代碼與示例 #11 完全相同。它實際上也是一種 IIFE,只是語法稍有不同。嗨,這是箭頭函數(shù)。
箭頭函數(shù)的意思是,這些函數(shù)中的“this”是一個詞法變量。也就是說,當將“this”與這種箭頭函數(shù)綁定時,函數(shù)會從包裹它的函數(shù)或作用域中獲取“this”的值。包裹我們這個箭頭函數(shù)的函數(shù)里面的“this”是 arr。因此?
// This is WGL
arr [1, 2, 3, 4]
Normal this : global
Arrow function this : arr [1, 2, 3, 4]
復制代碼如果我用箭頭函數(shù)重寫示例 #9 會怎么樣?控制臺輸出什么呢?
var arr = [1, 2, 3, 4];
arr.forEach(() => {
console.log(this);
});
復制代碼上面的這個例子是額外追加的,所以即使你猜對了也不用增加分數(shù)。你還在算分嗎?書呆子。
現(xiàn)在請仔細關注以下示例。我會不惜一切代價讓你弄懂他們 :-)。
Example #13
var yearlyExpense = {
year: 2016,
expenses: [
? {‘month’: ‘January’, amount: 1000},
? {‘month’: ‘February’, amount: 2000},
? {‘month’: ‘March’, amount: 3000}
? ],
printExpenses: function() {
? this.expenses.forEach(function(expense) {
? console.log(expense.amount + ‘ spent in ‘ + expense.month + ‘, ‘ +? ? this.year);
? });
? }
};
yearlyExpense.printExpenses();
復制代碼那么,輸出是?多點時間想一想。
這是答案,但我希望你在閱讀解釋之前先自己想想。
1000 spent in January, undefined?
2000 spent in February, undefined?
3000 spent in March, undefined
復制代碼這都是關于 printExpenses 函數(shù)的。首先注意下它是如何被調(diào)用的。隱式綁定?是的。所以 printExpenses 中的“this”指向的是對象 yearlycost。這意味著 this.expenses 是 yearlyExpense 對象中的 expenses 數(shù)組,所以這里沒有問題?,F(xiàn)在,當它在傳遞給 forEach 的回調(diào)函數(shù)中出現(xiàn)“this”時,它當然是全局對象,請參考例 #9。
注意,下面的“修正”版本是如何使用箭頭函數(shù)進行改進的。
var expense = {
year: 2016,
expenses: [
? {‘month’: ‘January’, amount: 1000},
? {‘month’: ‘February’, amount: 2000},
? {‘month’: ‘March’, amount: 3000}
? ],
printExpenses: function() {
? this.expenses.forEach((expense) => {
? ? console.log(expense.amount + ‘ spent in ‘ + expense.month + ‘, ‘ +? this.year);
? });
? }
};
expense.printExpenses();
復制代碼這樣我們就得到了想要的輸出結(jié)果:
1000 spent in January, 2016?
2000 spent in February, 2016?
3000 spent in March, 2016
復制代碼到目前為止,我們已經(jīng)熟悉了隱式綁定和默認綁定。我們現(xiàn)在知道函數(shù)被調(diào)用的方式?jīng)Q定了它里面的“this”。我們還簡要地講了箭頭函數(shù)以及它們內(nèi)部的“this”是怎樣定義的。
在我們討論其他規(guī)則之前,你應該知道,有些情況下,我們的“this”可能會丟失隱式綁定。讓我們快速地看一下這些例子。
Example #14
var obj = {?
a: 2,?
foo: function() {?
? console.log(this);?
}?
};
obj.foo();
var bar = obj.foo;?
bar();
復制代碼不要被這里面的花哨代碼所分心,只需注意函數(shù)是如何被調(diào)用的,就可以弄明白“this”的含義。你現(xiàn)在一定已經(jīng)掌握這個技巧了吧。首先 obj.foo() 被調(diào)用,因為 foo 前面有一個對象引用,所以首先輸出的是對象 obj。bar 當然是被獨立調(diào)用的,因此下一個輸出是全局變量。提醒你一下,記住在嚴格模式下,全局對象是不會默認綁定的,因此如果你在開啟了嚴格模式,那么控制臺輸出的就是 undefined,而不再是全局變量。
bar 和 foo 是對同一個函數(shù)的引用,唯一區(qū)別是它們被調(diào)用的方式不同。
Example #15
var obj = {?
a: 2,?
foo: function() {?
? console.log(this.a);?
}?
};
function doFoo(fn) {?
fn();?
}
doFoo(obj.foo);
復制代碼這里也沒什么特別的。我們是通過把 obj.foo 作為 doFoo 函數(shù)的參數(shù)(doFoo 這個名字聽起來很有趣)。同樣, fn 和 foo 是對同一個函數(shù)的引用?,F(xiàn)在我要重復同樣的分析過程, fn 被獨立調(diào)用,因此 fn 中的“this”是全局對象。而全局對象沒有屬性 a,因此我們在控制臺中得到了 undifined 的輸出結(jié)果。
到這里,我們這部分就講完了。在這一部分中,我們討論了將“this”綁定到函數(shù)的兩個規(guī)則。默認綁定和隱式綁定。我們研究了如何使用“use strict”來影響全局對象的綁定,以及如何會讓隱式綁定的“this”失效。我希望在接下來的第二部分中,你會發(fā)現(xiàn)本文對你有所幫助,在那里我們將介紹一些新規(guī)則,包括 new 和顯式綁定。那里再見吧!
在我們結(jié)束之前,我想用一個“簡單”的例子來作為這一部分的收尾,當我開始使用 Javascript 時,這個例子曾經(jīng)讓我感到非常震驚。Javascript 里面也并不是所有的東西都是美的,也有看起來很糟糕的東西。讓我們看看其中的一個。
var obj = {?
a: 2,?
b: this.a * 2?
};
console.log( obj.b ); // NaN
復制代碼它讀起來感覺很好,在 obj 里面,“this”應該是 obj,因此是 this.a 應該是 2。嗯,錯了。因為在這個對象里面的“this”是全局對象,所以如果你像這么寫…
var myObj = {?
a: 2,?
b: this?
};
console.log(myObj.b); // global
復制代碼控制臺輸出的就是全局對象。你可能會說“但是,myObj 是全局對象的屬性(示例 #4 和示例 #8),不對嗎?”是的,絕對正確。
console.log( this === myObj.b ); // true?
console.log( this.hasOwnProperty(‘myObj’) ); //true
復制代碼“也就是說,如果我像這樣寫的話,它就可以!”
var myObj = {?
a: 2,?
b: this.myObj.a * 2?
};
復制代碼遺憾的是,不是這樣的,這會導致邏輯錯誤。上面的代碼是不正確的,編譯器會抱怨它找不到未定義的屬性 a。為什么會這樣?我也不太清楚。
幸運的是,getters(隱式綁定)可以給我們提供幫助。
var myObj = {?
a: 2,?
get b() {?
? return this.a * 2?
}?
};
console.log( myObj.b ); // 4
復制代碼你堅持到最后了!做得好。第二部分,我們再見。