- 原文地址:Let’s settle ‘this’ — Part One
- 原文做者:Nash Vail
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:geniusq1981
- 校對者:Moonliujk、lance10030
難道咱們就不能完全搞清楚「this」嗎?在某種程度上,幾乎全部的 JavaScript 開發人員都曾經思考過「this」這個事情。對我來講,每當「this」出來搗亂的時候,我就會千方百計地去解決掉它,但事後就把它忘了,我想你應該也曾遇到過相似的場景。可是今天,讓咱們弄明白它,讓咱們一次性地完全解決「this」的問題,一勞永逸。前端
前幾天,我在圖書館遇到了一個意想不到的事情。android
這本書的整個第二章都是關於「this」的,我頗有自信地通讀了一遍,可是發現其中有些地方講到的「this」,我竟然搞不懂它們是什麼,須要去猜想。真的是時候檢討一下我過分自信的愚蠢行爲了。我再次把這一章重讀了好幾遍,發覺這裏面的內容是每一個 Javascript 開發人員都應該瞭解的。ios
所以,我嘗試着用一種更完全的方式和更多的示例代碼來展現 凱爾·辛普森 在他的這本書 你不知道的 Javascript 中描述的那些規範。git
在這裏我不會通篇只講理論,我會直接以曾經困擾過個人困難問題爲例開始講起,我但願它們也是你感到困難的問題。但無論這些問題是否會困撓你,我都會給出解釋說明,我會一個接一個地向你介紹全部的規則,固然還會有一些追加內容。github
在開始以前,我假設你已經瞭解了一些 JavaScript 的背景知識,當我講到 global、window、this、prototype 等等的時候,你知道它們是什麼意思。這篇文章中,我會同時使用 global 和 window,在這裏它們就是一回事,是能夠互換的。後端
在下面給出的全部代碼示例中,你的任務就是猜一下控制檯輸出的結果是什麼。若是你猜對了,就給你本身加一分。準備好了嗎?讓咱們開始吧。數組
function foo() {
console.log(this);
bar();
}
function bar() {
console.log(this);
baz();
}
function baz() {
console.log(this);
}
foo();
複製代碼
你被難住了嗎?爲了測試,你固然能夠把這段代碼複製下來,而後在瀏覽器或者 Node 的運行環境中去運行看看結果。再來一次,你被難住了嗎?好吧,我就再也不問了。但說真的,若是你沒被難住,那就給你本身加一分。瀏覽器
若是你運行上面的代碼,就會在控制檯中看到 global 對象被打印出三次。爲了解釋這一點,讓我來介紹 第一個規則,默認綁定。規則規定,當一個函數執行獨立調用時,例如只是 funcName();,這時函數的「this」被指向 global 對象。函數
須要理解的是,在調用函數以前,「this」並無綁定到這個函數,所以,要找到「this」,你應該密切注意該函數是如何調用,而不是在哪裏調用。全部三個函數 foo();bar(); 和 baz();_ 都是獨立的調用,所以這三個函數的「this」都指向全局對象。post
‘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。
回顧一下,在一個簡單調用函數中,好比獨立調用中,「this」在非嚴格模式下指向 global 對象,但在嚴格模式下不容許 global 對象默認綁定,所以這些函數中的「this」是 undefined。
爲了使咱們對默認綁定概念理解得更加具體,這裏有一些示例。
function foo() {
function bar() {
console.log(this);
}
bar();
}
foo();
複製代碼
foo 先被調用,而後又調用 bar,bar 將「this」打印到控制檯中。這裏的技巧是看看函數是如何被調用的。foo 和 bar 都被單獨調用,所以,他們內部的「this」都是指向 global 對象。可是因爲 bar 是惟一執行打印的函數,因此咱們看到 global 對象在控制檯中輸出了一次。
我但願你沒有回答 foo 或 bar。有沒有?
咱們已經瞭解了默認綁定。讓咱們再作一個簡單的測試。在下面的示例中,控制檯輸出什麼?
var a = 1;
function foo() {
console.log(this.a);
}
foo();
複製代碼
輸出結果是 undefined?是 1?仍是什麼?
若是你已經很好地理解了以前講解的內容,那麼你應該知道控制檯輸出的是「1」。爲何?首先,默認綁定做用於函數 foo。所以 foo 中的「this」指向 global 對象,而且 a 被聲明爲 global 變量,這就意味着 a 是 global 對象的屬性(也稱之爲全局對象污染),所以 this.a 和 var a 就是同一個東西。
隨着本文的深刻,咱們將會繼續研究默認綁定,可是如今是時候向你介紹下一個規則了。
var obj = {
a: 1,
foo: function() {
console.log(this);
}
};
obj.foo();
複製代碼
這裏應該沒有什麼疑問,對象「obj」會被輸出在控制檯中。你在這裏看到的是 隱式綁定。規則規定,當一個函數被做爲一個對象方法被調用時,那麼它內部的「this」應該指向這個對象。若是函數調用前面有多個對象( obj1.obj2.func() ),那麼函數以前的最後一個對象(obj3)會被綁定。
須要注意的一點是函數調用必須有效,那也就是說當你調用 obj.func() 時,必須確保 func 是對象 obj 的屬性。
所以,在上面的例子中調用 obj.foo() 時,「this」就指向 obj,所以 obj 被打印輸出在控制檯中。
function logThis() {
console.log(this);
}
var myObject = {
a: 1,
logThis: logThis
};
logThis();
myObject.logThis();
複製代碼
你被難住了?我但願沒有。
跟在 myObject 後面的這個全局調用 logThis() 經過 console.log(this) 打印的是 global 對象;而 myObject.logThis() 打印的是 myObject 對象。
這裏須要注意一件有趣的事情:
console.log(logThis === myObject.logThis); // true
複製代碼
爲何不呢?它們固然是相同的函數,可是你能夠看到 如何調用_logThis_ 會讓其中的「this」發生改變。當 logThis 被單獨調用時,使用默認綁定規則,可是當 logThis 做爲前面的對象屬性被調用時,使用隱式綁定規則。
無論採用哪條規則,讓咱們看看是怎麼處理的(雙關語)。
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo();
複製代碼
控制檯輸出什麼?首先,你可能會問咱們能夠調用「_this.bar()」嗎?固然能夠,它不會致使錯誤。
就像示例 #4 中的 var a 同樣,bar 也是全局對象的屬性。由於 foo 被單獨調用了,它內部的「this」就是全局對象(默認綁定規則)。所以 foo 內部的 this.bar 就是 bar。但實際的問題是,控制檯中輸出什麼?
若是你猜的沒錯,「undefined」會被打印出來。
注意 bar 是如何被調用的?看起來,隱式綁定在這裏發揮做用。隱式綁定意味着 bar 中的「this」是其前面的對象引用。bar 前面的對象引用是全局對象,在 foo 裏面是全局對象,對不對?所以在 bar 中嘗試訪問 this.a 等同於訪問 [global object].a。沒有什麼意外,所以控制檯會輸出 undefined。
太棒了!繼續向下講解。
var obj = {
a: 1,
foo: function(fn) {
console.log(this);
fn();
}
};
obj.foo(function() {
console.log(this);
});
複製代碼
請不要讓我失望。
函數 foo 接受一個回調函數做爲參數。咱們所作的就是在調用 foo 的時候在參數裏面放了一個函數。
obj.foo( function() { console.log(this); } );
複製代碼
可是請注意 foo 是 如何 被調用的。它是一個單獨調用嗎?固然不是,所以第一個輸出到控制檯的是對象 obj 。咱們傳入的回調函數是什麼?在 foo 內部,回調函數變爲 fn ,注意 fn 是 如何 被調用的。對,所以 fn 中的「this」是全局對象,所以第二個被輸出到控制檯的是全局對象。
但願你不會以爲無聊。順便問一下,你的分數怎麼樣?還能夠嗎?好吧,此次我準備難倒你了。
var arr = [1, 2, 3, 4];
Array.prototype.myCustomFunc = function() {
console.log(this);
};
arr.myCustomFunc();
複製代碼
若是你還不知道 Javascript 裏面的 .prototype 是什麼,那你就權且把它和其餘對象等同看待,但若是你是 JavaScript 開發者,你應該知道。你知道嗎?努努力,再去多讀一些關於原型鏈相關的書籍吧。我在這裏等着你。
那麼打印輸出的是什麼?是 Array.prototype 對象?錯了!
這是和以前相同的技巧,請檢查 custommyfunc 是 如何 被調用的。沒錯,隱式綁定把 arr 綁定到 myCustomFunc,所以輸出到控制檯的是 arr[1,2,3,4]。
我說的,你理解了嗎?
var arr = [1, 2, 3, 4];
arr.forEach(function() {
console.log(this);
});
複製代碼
執行上述代碼的結果是,在控制檯中輸出了 4 次全局對象。若是你錯了,也不要緊。請再看示例#7。還沒理解?下一個示例會有所幫助。
var arr = [1, 2, 3, 4];
Array.prototype.myCustomFunc = function(fn) {
console.log(this);
fn();
};
arr.myCustomFunc(function() {
console.log(this);
});
複製代碼
就像示例 #7 同樣,咱們將回調函數 fn 做爲參數傳遞給函數 myCustomFunc。結果是傳入的函數會被獨立調用。這就是爲何在前面的示例(#9)中輸出全局對象,由於在 forEach 中傳入的回調函數被獨立調用。
相似地,在本例中,首先輸出到控制檯的是 arr,而後是輸出的是全局對象。我知道這看上去有點複雜,但我相信若是你能再多用點心,你會弄明白的。
讓咱們繼續使用這個數組的示例來介紹更多的概念。我想我會在這裏使用一個簡稱,WGL 怎麼樣?做爲 WHAT.GETS.LOGGED 的簡稱?好吧,在我開始老生常談以前,下面是另一個例子。
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(當即調用的函數表達式)。這個名字不用再過多解釋了,對吧?被 (…)(); 這樣形式封裝的函數會當即被調用,也就是說等同於被獨立調用,所以它內部的「this」是全局變量,因此輸出的是全局變量。
要來新概念了!讓咱們看看你對 ES2015 的熟悉程度。
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,只是語法稍有不一樣。嗨,這是箭頭函數。
箭頭函數的意思是,這些函數中的「this」是一個詞法變量。也就是說,當將「this」與這種箭頭函數綁定時,函數會從包裹它的函數或做用域中獲取「this」的值。包裹咱們這個箭頭函數的函數裏面的「this」是 arr。所以?
// This is WGL
arr [1, 2, 3, 4]
Normal this : global
Arrow function this : arr [1, 2, 3, 4]
複製代碼
若是我用箭頭函數重寫示例 #9 會怎麼樣?控制檯輸出什麼呢?
var arr = [1, 2, 3, 4];
arr.forEach(() => {
console.log(this);
});
複製代碼
上面的這個例子是額外追加的,因此即便你猜對了也不用增長分數。你還在算分嗎?書呆子。
如今請仔細關注如下示例。我會不惜一切代價讓你弄懂他們 :-)。
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 函數的。首先注意下它是如何被調用的。隱式綁定?是的。因此 printExpenses 中的「this」指向的是對象 yearlycost。這意味着 this.expenses 是 yearlyExpense 對象中的 expenses 數組,因此這裏沒有問題。如今,當它在傳遞給 forEach 的回調函數中出現「this」時,它固然是全局對象,請參考例 #9。
注意,下面的「修正」版本是如何使用箭頭函數進行改進的。
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();
複製代碼
這樣咱們就獲得了想要的輸出結果:
1000 spent in January, 2016
2000 spent in February, 2016
3000 spent in March, 2016
複製代碼
到目前爲止,咱們已經熟悉了隱式綁定和默認綁定。咱們如今知道函數被調用的方式決定了它裏面的「this」。咱們還簡要地講了箭頭函數以及它們內部的「this」是怎樣定義的。
在咱們討論其餘規則以前,你應該知道,有些狀況下,咱們的「this」可能會丟失隱式綁定。讓咱們快速地看一下這些例子。
var obj = {
a: 2,
foo: function() {
console.log(this);
}
};
obj.foo();
var bar = obj.foo;
bar();
複製代碼
不要被這裏面的花哨代碼所分心,只需注意函數是如何被調用的,就能夠弄明白「this」的含義。你如今必定已經掌握這個技巧了吧。首先 obj.foo() 被調用,由於 foo 前面有一個對象引用,因此首先輸出的是對象 obj。bar 固然是被獨立調用的,所以下一個輸出是全局變量。提醒你一下,記住在嚴格模式下,全局對象是不會默認綁定的,所以若是你在開啓了嚴格模式,那麼控制檯輸出的就是 undefined,而再也不是全局變量。
bar 和 foo 是對同一個函數的引用,惟一區別是它們被調用的方式不一樣。
var obj = {
a: 2,
foo: function() {
console.log(this.a);
}
};
function doFoo(fn) {
fn();
}
doFoo(obj.foo);
複製代碼
這裏也沒什麼特別的。咱們是經過把 obj.foo 做爲 doFoo 函數的參數(doFoo 這個名字聽起來頗有趣)。一樣, fn 和 foo 是對同一個函數的引用。如今我要重複一樣的分析過程, fn 被獨立調用,所以 fn 中的「this」是全局對象。而全局對象沒有屬性 a,所以咱們在控制檯中獲得了 undifined 的輸出結果。
到這裏,咱們這部分就講完了。在這一部分中,咱們討論了將「this」綁定到函數的兩個規則。默認綁定和隱式綁定。咱們研究瞭如何使用「use strict」來影響全局對象的綁定,以及如何會讓隱式綁定的「this」失效。我但願在接下來的第二部分中,你會發現本文對你有所幫助,在那裏咱們將介紹一些新規則,包括 new 和顯式綁定。那裏再見吧!
在咱們結束以前,我想用一個「簡單」的例子來做爲這一部分的收尾,當我開始使用 Javascript 時,這個例子曾經讓我感到很是震驚。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
複製代碼
你堅持到最後了!作得好。第二部分,咱們再見。
若是你發現這篇文章頗有用,你能夠推薦並分享給其餘開發者。我常常發表文章,在 Twitter 和 Medium 上關注我,以便在這種狀況發生時獲得通知。
謝謝你的閱讀,祝你愉快!
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。