[譯] 讓咱們一塊兒解決「this」難題 — 第一部分

難道咱們就不能完全搞清楚「this」嗎?在某種程度上,幾乎全部的 JavaScript 開發人員都曾經思考過「this」這個事情。對我來講,每當「this」出來搗亂的時候,我就會千方百計地去解決掉它,但事後就把它忘了,我想你應該也曾遇到過相似的場景。可是今天,讓咱們弄明白它,讓咱們一次性地完全解決「this」的問題,一勞永逸。前端

前幾天,我在圖書館遇到了一個意想不到的事情。android

這本書的整個第二章都是關於「this」的,我頗有自信地通讀了一遍,可是發現其中有些地方講到的「this」,我竟然搞不懂它們是什麼,須要去猜想。真的是時候檢討一下我過分自信的愚蠢行爲了。我再次把這一章重讀了好幾遍,發覺這裏面的內容是每一個 Javascript 開發人員都應該瞭解的。ios

所以,我嘗試着用一種更完全的方式和更多的示例代碼來展現 凱爾·辛普森 在他的這本書 你不知道的 Javascript 中描述的那些規範。git

在這裏我不會通篇只講理論,我會直接以曾經困擾過個人困難問題爲例開始講起,我但願它們也是你感到困難的問題。但無論這些問題是否會困撓你,我都會給出解釋說明,我會一個接一個地向你介紹全部的規則,固然還會有一些追加內容。github

在開始以前,我假設你已經瞭解了一些 JavaScript 的背景知識,當我講到 global、window、this、prototype 等等的時候,你知道它們是什麼意思。這篇文章中,我會同時使用 global 和 window,在這裏它們就是一回事,是能夠互換的。後端

在下面給出的全部代碼示例中,你的任務就是猜一下控制檯輸出的結果是什麼。若是你猜對了,就給你本身加一分。準備好了嗎?讓咱們開始吧。數組

Example #1

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

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

回顧一下,在一個簡單調用函數中,好比獨立調用中,「this」在非嚴格模式下指向 global 對象,但在嚴格模式下不容許 global 對象默認綁定,所以這些函數中的「this」是 undefined。

爲了使咱們對默認綁定概念理解得更加具體,這裏有一些示例。

Example #3

function foo() {
 function bar() {
  console.log(this); 
 } 
 bar();
}

foo();
複製代碼

foo 先被調用,而後又調用 barbar 將「this」打印到控制檯中。這裏的技巧是看看函數是如何被調用的。foobar 都被單獨調用,所以,他們內部的「this」都是指向 global 對象。可是因爲 bar 是惟一執行打印的函數,因此咱們看到 global 對象在控制檯中輸出了一次。

我但願你沒有回答 foobar。有沒有?

咱們已經瞭解了默認綁定。讓咱們再作一個簡單的測試。在下面的示例中,控制檯輸出什麼?

Example #4

var a = 1;

function foo() {  
 console.log(this.a);  
}

foo();
複製代碼

輸出結果是 undefined?是 1?仍是什麼?

若是你已經很好地理解了以前講解的內容,那麼你應該知道控制檯輸出的是「1」。爲何?首先,默認綁定做用於函數 foo。所以 foo 中的「this」指向 global 對象,而且 a 被聲明爲 global 變量,這就意味着 a 是 global 對象的屬性(也稱之爲全局對象污染),所以 this.avar a 就是同一個東西。

隨着本文的深刻,咱們將會繼續研究默認綁定,可是如今是時候向你介紹下一個規則了。

Example #5

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 被打印輸出在控制檯中。

Example #6

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 做爲前面的對象屬性被調用時,使用隱式綁定規則。

無論採用哪條規則,讓咱們看看是怎麼處理的(雙關語)。

Example #8

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。

太棒了!繼續向下講解。

Example #7

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」是全局對象,所以第二個被輸出到控制檯的是全局對象。

但願你不會以爲無聊。順便問一下,你的分數怎麼樣?還能夠嗎?好吧,此次我準備難倒你了。

Example #8

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]

我說的,你理解了嗎?

Example #9

var arr = [1, 2, 3, 4];

arr.forEach(function() {  
 console.log(this);  
});
複製代碼

執行上述代碼的結果是,在控制檯中輸出了 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 同樣,咱們將回調函數 fn 做爲參數傳遞給函數 myCustomFunc。結果是傳入的函數會被獨立調用。這就是爲何在前面的示例(#9)中輸出全局對象,由於在 forEach 中傳入的回調函數被獨立調用。

相似地,在本例中,首先輸出到控制檯的是 arr,而後是輸出的是全局對象。我知道這看上去有點複雜,但我相信若是你能再多用點心,你會弄明白的。

讓咱們繼續使用這個數組的示例來介紹更多的概念。我想我會在這裏使用一個簡稱,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(當即調用的函數表達式)。這個名字不用再過多解釋了,對吧?被 (…)(); 這樣形式封裝的函數會當即被調用,也就是說等同於被獨立調用,所以它內部的「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,只是語法稍有不一樣。嗨,這是箭頭函數。

箭頭函數的意思是,這些函數中的「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);
});
複製代碼

上面的這個例子是額外追加的,因此即便你猜對了也不用增長分數。你還在算分嗎?書呆子。

如今請仔細關注如下示例。我會不惜一切代價讓你弄懂他們 :-)。

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 函數的。首先注意下它是如何被調用的。隱式綁定?是的。因此 printExpenses 中的「this」指向的是對象 yearlycost。這意味着 this.expensesyearlyExpense 對象中的 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」可能會丟失隱式綁定。讓咱們快速地看一下這些例子。

Example #14

var obj = {  
 a: 2,  
 foo: function() {  
  console.log(this);  
 }  
};

obj.foo();

var bar = obj.foo;  
bar();
複製代碼

不要被這裏面的花哨代碼所分心,只需注意函數是如何被調用的,就能夠弄明白「this」的含義。你如今必定已經掌握這個技巧了吧。首先 obj.foo() 被調用,由於 foo 前面有一個對象引用,因此首先輸出的是對象 objbar 固然是被獨立調用的,所以下一個輸出是全局變量。提醒你一下,記住在嚴格模式下,全局對象是不會默認綁定的,所以若是你在開啓了嚴格模式,那麼控制檯輸出的就是 undefined,而再也不是全局變量。

bar 和 foo 是對同一個函數的引用,惟一區別是它們被調用的方式不一樣。

Example #15

var obj = {  
 a: 2,  
 foo: function() {  
  console.log(this.a);  
 }  
};

function doFoo(fn) {  
 fn();  
}

doFoo(obj.foo);
複製代碼

這裏也沒什麼特別的。咱們是經過把 obj.foo 做爲 doFoo 函數的參數(doFoo 這個名字聽起來頗有趣)。一樣, fnfoo 是對同一個函數的引用。如今我要重複一樣的分析過程, 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
複製代碼

你堅持到最後了!作得好。第二部分,咱們再見。

若是你發現這篇文章頗有用,你能夠推薦並分享給其餘開發者。我常常發表文章,在 TwitterMedium 上關注我,以便在這種狀況發生時獲得通知。

謝謝你的閱讀,祝你愉快!

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索