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

嗨!歡迎來到讓咱們一塊兒解決「this」難題的第二部分,咱們試圖揭開 JavaScript 中最難讓人理解的一部份內容 - 「this」關鍵字的神祕面紗。若是您尚未讀過 第一部分,你須要先把它讀一下。在第一部分中,咱們經過 15 個示例介紹了默認綁定規則和隱式綁定規則。咱們瞭解了函數內部的「this」如何隨着函數調用方式的不一樣而發生改變。最後,咱們也介紹了箭頭函數以及它是如何進行詞法綁定。我但願你能記住這一切。前端

在這一部分咱們將討論兩個新規則,從 new 綁定開始,咱們將深刻地分析這一切是如何工做的。接下來,咱們將介紹顯式綁定以及如何經過 call(...),apply(...) 和 bind(...) 方法將任意對象綁定到函數內部的「this」上。android

讓咱們接着以前的內容繼續。你的任務仍是同樣,繼續猜一下控制檯的輸出內容是什麼。還記得 WGL 嗎?ios

不過,在深刻以前,先讓咱們經過一個例子來熱熱身。git

Example #16

function foo() {}

foo.a = 2;
foo.bar = {
 b: 3,
 c: function() {
  console.log(this);
 } 
}

foo.bar.c();
複製代碼

我知道,如今你可能會想「到底發生了什麼?爲何在這裏將屬性分配給函數?這不會致使錯誤嗎?」好吧,首先,這不會致使錯誤。JavaScript 中的每一個函數也都是一個對象。就像其餘普通的對象同樣,你也能夠爲函數指定屬性!程序員

接下來,讓咱們弄清楚控制檯會輸出什麼。若是您注意下,你會發現隱式綁定在此處起做用。c 調用以前的對象是 bar,對嗎?所以 c 中的「this」指向的是 bar,所以 bar 被輸出到控制檯中。github

經過這個示例,你能夠知道,JavaScript 中的函數也是對象,就像任何其餘對象同樣,它們能夠被賦予屬性。後端

Example #17

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

new foo();
複製代碼

那麼,輸出什麼?仍是根本沒有輸出?數組

正確答案是一個空對象。是的,不是 a,也不是 foo,只是一個空對象。讓咱們看看它是如何工做的。app

首先要注意,函數 如何 被調用。它不是一個獨立調用,它的前面也沒有對象引用。它的前面只有一個 new。在 Javascript 中能夠經過 new 關鍵字來引入任意函數。當這樣作的時候,使 new 引入一個函數時,大體會發生四件事情,其中兩個是,函數

  1. 建立一個空對象。
  2. 新建立的對象被綁定到函數調用的「this」上。

第二點正是你執行上面的代碼時控制檯輸出一個空對象的緣由。你可能會問「這能有什麼用?」。咱們會發現這裏有些小爭議。

Example #18

function foo(id, name) {
 this.id = id;
 this.name = name;
}

foo.prototype.print = function() {
 console.log( this.id, this.name );
};

var a = new foo(1, ‘A’);
var b = new foo(2, ‘B’);

a.print();
b.print();
複製代碼

直觀地說,在這個例子中很容易就能猜到控制檯上輸出什麼,可是從技術角度你知道真正的原理嗎?讓咱們來看看。

來回顧一下,當使用 new 關鍵字調用函數時,會發生四個事件。

  1. 建立一個空對象。
  2. 新建立的對象被綁定到函數調用的「this」上。
  3. 新建立對象的原型鏈指向函數的原型對象。
  4. 函數被正常執行,最後返回新建立的對象。

在前面的例子中咱們已經驗證了前兩個事情,這就是咱們會在控制檯中輸出空對象的緣由。先忘掉第三點,讓咱們聚焦在第四點上。沒有什麼能夠阻止函數的執行,除了函數內部的「this」是新建立的空對象以外,傳參後函數的執行過程與其餘正常的 Javascript 函數同樣。所以,這個例子中的 foo,在它裏面咱們執行相似 this.id=id 的操做時,咱們其實是將屬性分配給了在調用函數時綁定到「this」上的新建立的空對象。再讀一遍這句話。一旦函數執行完成,就會返回這個剛被建立的對象。因爲在上面的示例中咱們爲返回的對象分配了 idname 屬性,因此這個返回的對象也會擁有這些屬性。而後咱們能夠將返回的對象賦值給咱們想要的任何變量,就像咱們上面示例中的 a 和 b。

每一個使用 new 關鍵字的函數調用都會建立一個全新的空對象,在函數內部配置對象的參數屬性 _(this.propName = …) 在函數執行完畢後返回這個對象。

var a = {
 id: 1,
 name: ‘A’
};

var b = {
 id: 2,
 name: ‘B’
};
複製代碼

太棒了!咱們剛剛學會了建立對象的新方法。可是 a 和 b 有一些共同點,它們都是 原型鏈指向 foo 的原型對象(事件 4),所以能夠訪問它們的屬性(變量,函數等等)。正由於如此,咱們能夠調用 a.print()b.print(),由於 print 是咱們在 foo 原型鏈上建立的函數。快速的問一個問題,當我調用 a.print() 時會發生什麼綁定?若是你說發生了隱性綁定,那你就答對了。所以,在調用 a.print() 時,print 裏面的「this」指向的就是 a,而且控制檯上首先輸出的是 1,A,一樣當咱們調用 b.print() 時,會輸出 2,B

Example #19

function foo(id, name) {
 this.id = id;
 this.name = name;

 return {
  message: ‘Got you!’
 };
}

foo.prototype.print = function() {
 console.log( this.id, this.name );
};

var a = new foo(1, ‘A’);
var b = new foo(2, ‘B’);

console.log( a );
console.log( b );
複製代碼

幾乎與上一個示例中的代碼徹底相同,除了請注意,foo 函數如今返回的是一個對象。好吧,讓咱們返回上一個例子,重讀一下第四點,怎麼樣?注意加粗的內容了嗎?當使用 new 關鍵字調用函數時,在執行結束時將返回新建立的對象,除非你返回自定義對象,就像咱們在這個示例中所作的這樣。

因此?輸出的什麼?很明顯,它返回自定義對象,具備 message 屬性的這個對象會在控制檯中輸出,輸出兩次。如此容易就打破了整個結構,是否是?只返回了一個沒有意義的對象,一切就徹底改變了。此外,你如今沒法調用 a.print()b.print(),由於 ab 被分配了返回的對象,但返回的對象沒有連接到 foo 的原型鏈。

但等一下,若是不返回一個對象,咱們返回好比 'abc'、數字、布爾值、函數、nullundefined 或是數組,結果會怎樣?事實證實,構造對象是否會改變取決於你返回的內容。看看下面的模式?

return {}; // 改變
return function() {}; // 改變
return new Number(3); // 改變
return [1, 2, 3]; // 改變
return null; // 不改變
return undefined; // 不改變
return ‘Hello’; // 不改變
return 3; // 不改變
...
複製代碼

爲何會這樣呢,這就是另一篇文章的主題了。個人意思是咱們已經離題有點遠了,這個例子與「this」綁定沒太大關係,對嗎?

在 Javascript 中,從好久以前就開始經過使用 new 關鍵字綁定來建立完整的對象(也許是一種誤用),以此來僞造傳統的類。實際上,在 JavaScript 中沒有類的概念,ES2015 中新的 class 語法只是一個語法。在它的後面仍是使用 new 綁定,沒有任何變化。我一點都不關心你是否使用 new 綁定僞造類,只要你的程序工做正常,代碼是可擴展,可讀和可維護的,就沒有問題。可是,因爲 new 綁定帶來的不穩定性,你如何可以確保全部代碼包都擁有可擴展,可讀和可維護的代碼呢?

可能這裏還涉及不少內容。若是你還有點迷茫,你應該再從新閱讀一下。重要的是若是你瞭解了 new 綁定的工做原理,可能永遠都不會再使用它 :)。

不開玩笑,讓咱們繼續。

思考如下的代碼。不用猜想這個例子會輸出什麼,咱們將從下個例子開始繼續「猜謎遊戲」 :)。

var expenses = {
 data: [1, 2, 3, 4, 5],
 total: function(earnings) {
  return this.data.reduce( (prev, cur) => prev + cur ) - (earnings || 0);
 }
};

var rents = {
 data: [1, 2, 3, 4]
};
複製代碼

expenses 對象具備 datatotal 兩個屬性。data 包含一些數字,而 total 是一個函數,它將 earnings 做爲輸入參數並返回 data 中全部數字的總和減去 earnings。很是直觀。

如今看一下 rents,就像 expenses 同樣,它也有 data 屬性。這樣說,出於某種緣由,這只是個假設,你想基於 rentdata 數組運行 total 函數,由於咱們是優秀的程序員,咱們不喜歡重複工做。咱們絕對沒法調用 rents.total(),也沒法把 rents 的「this」隱式綁定爲 total,由於 rents.total() 是一個無效的調用,由於 rents 沒有名爲 total 的屬性。如今有沒有一種方法能夠將 rents 的「this」綁定爲 total 函數。好吧,猜猜是什麼?是有的,請容許我介紹 call()apply()

你能夠看到 callapply 作了一樣的事情,它們容許你將你想要的對象綁定到你想要的功能上。這意味着我能夠作到這一點……

console.log( expenses.total.call(rents) ); // 10
複製代碼

還有這個。

console.log( expenses.total.apply(rents) ); // 10
複製代碼

這很棒!上面的兩行代碼都會致使 total 函數被調用,而內部的「this」被綁定爲 rents 對象。callapply 兩個方法就「this」綁定而言,只有傳遞參數的方式不一樣。

注意,total 函數有一個參數 earnings,讓咱們傳一下參數試試。

console.log( expenses.total.call(rents, 10) ); // 0 正常!
console.log( expenses.total.apply(rents, 10) ); // 報錯
複製代碼

使用 call 給目標函數(在咱們的例子中是 total )傳遞參數很簡單,像給其餘普通函數傳遞參數同樣,你只需傳入一個由逗號隔開的參數列表 .call(customThis, arg1, arg2, arg3…)。在上面的代碼咱們傳入了 10 做爲 earnings 參數,一切正常。

apply 要求你將參數傳遞給目標函數(在咱們的例子中是 total)時,將參數包裝在一個數組裏 .apply(customThis,[arg1,arg2,arg3 ...]) 你應該注意到了,上面的代碼中咱們沒有這樣傳入參數,因此會發生錯誤。把參數封裝成一個數組,而後再傳入,就不會報錯了。就像下面這樣。

console.log( expenses.total.apply(rents, [10]) ); // 0 正常!
複製代碼

我過去曾經總結了一個助記符就是經過上面說的這點差異來記住 callapply 之間的區別的。A 表明 apply ,A 也表明 array !因此經過 apply 把參數傳給目標函數時,須要把參數封裝成 array 。這只是一個簡單的小助記符,但它確實頗有用。

如今若是咱們傳入一個數字,或一個字符串,或一個布爾值,或 null/undefined,而不是傳入一個對象來調用 callapplybind (接下來討論)。那樣會發生什麼?沒有什麼特別,好比你給「this」傳入數字 2, 它在對象內被封裝成對象形式 new Number(2) ,一樣若是你傳入一個字符串,它會變成 new String(...) ,布爾值會變成 new Boolean(...) 等等,這個新對象,不論是字符,仍是數字或是布爾值都被綁定到被調用函數的「this」。傳入 nullundefined 的結果會有點不一樣。若是調用函數時爲「this」傳入 nullundefined ,那它就好像進行了默認綁定同樣,那意味着全局對象被綁定在被調用函數的「this」上。

還有另外一種方法將'this'綁定到一個函數,此次經過一個方法名叫,等等,bind

讓咱們看看你是否能夠解決這個問題。下面的示例會輸出什麼?

Example #2

var expenses = {
 data: [1, 2, 3, 4, 5],
 total: function(earnings) {
  return this.data.reduce( (prev, cur) => prev + cur ) - (earnings   || 0);
 }
};

var rents = {
 data: [1, 2, 3, 4]
};

var rentsTotal = expenses.total.bind(rents);

console.log(rentsTotal());
console.log(rentsTotal(10));
複製代碼

這個例子的答案是 10 後跟着輸出 0。注意 rents 對象聲明下面發生了什麼。咱們從函數 expenses.total 建立一個新函數 rentsTotal 。這裏 bind 建立一個新函數,當這個函數被調用時,它的「this」關鍵字設置爲提供的值(在咱們的例子中是 rents )。所以,當咱們調用 rentsTotal() 時,雖然它是一個獨立的調用,但它的「this」已指向了 rents ,而默認綁定沒法覆蓋它。此次調用會在控制檯輸入 10。

在下一行中,使用參數(10)調用 rentsTotal 與使用相同的參數(10)調用 expenses.total 徹底相同,它只是「this」中的值不一樣。此次調用的結果爲 0。

另外,你也可使用 bind 綁定參數給目標函數(在咱們的例子中是 expenses.total)。思考下這個。

var rentsTotal = expenses.total.bind(rents, 10);
console.log(rentsTotal());
複製代碼

你認爲控制檯輸出什麼?固然是 0,由於 10 已經過 bind 綁定到目標函數(expenses.total)做爲 earnings 參數。

讓咱們看一個例子,它能夠說明 bind 生命週期。

Example #21

// HTML

<button id=」button」>Hello</button>

// JavaScript

var myButton = {
 elem: document.getElementById(‘button’),
 buttonName: ‘My Precious Button’,
 init: function() {
  this.elem.addEventListener(‘click’, this.onClick);
 },
 onClick: function() {
  console.log(this.buttonName);
 }
};

myButton.init();
複製代碼

咱們已經在 HTML 中建立了一個按鈕,而後咱們在 Javascript 代碼中,將這個按鈕定義爲 myButton 。注意,在 init 中,咱們還爲按鈕上添加了一個鼠標點擊的事件監聽。你如今的問題是當點擊按鈕的時候,控制檯會輸出什麼?

若是您猜對了,被打印出來的就是 undefined 。這種「奇怪的結果」的緣由是做爲事件監聽的回調(在咱們的例子中是 this.onClick),它會把目標元素綁定在「this」上。這意味着,當 onClick 被調用時,它內部的「this」是按鈕的 DOM 對象(elem),而不是咱們的 myButton 對象,由於按鈕的 DOM 對象沒有 buttonName 的屬性,因此控制檯輸出 undefined

可是有辦法解決這個問題(雙關語)。咱們須要作的就是添加一行代碼,僅需一行代碼。

方案 #1

var myButton = {
 elem: document.getElementById(‘button’),
 buttonName: ‘My Precious Button’,
 init: function() {
  this.onClick = this.onClick.bind(this);
  this.elem.addEventListener(‘click’, this.onClick);
 },
 onClick: function() {
  console.log(this.buttonName);
 }
};
複製代碼

注意上面的代碼片斷(#21)中調用函數 init 的方式。確切地說,隱式綁定將 myButton 綁定到 init 函數的「this」上。如今注意,咱們新加的代碼行是如何把 myButton 綁定到 onClick 函數。這樣作會建立一個新的函數,除了它內部的「this」指向了 myButton,其餘就和 onClick 徹底同樣。而後新建立的函數被從新分配給 myButton.onClick。這就是所有操做,當你點擊按鈕時,你將看到控制檯上輸出「My Precious Button」。

你也能夠經過箭頭函數來修復代碼。就是這樣。我將把這個問題留給你,讓你思考一下這爲何能夠。

方案 #2

var myButton = {
 elem: document.getElementById(‘button’),
 buttonName: ‘My Precious Button’,
 init: function() {
  this.elem.addEventListener(‘click’, () => {
   this.onClick.call(this);
  });
 },
 onClick: function() {
 console.log(this.buttonName);
 }
};
複製代碼

方案 #3

var myButton = {
 elem: document.getElementById(‘button’),
 buttonName: ‘My Precious Button’,
 init: function() {
  this.elem.addEventListener(‘click’, () => {
   console.log(this.buttonName);
  });
 }
};
複製代碼

好了。咱們差很少就要結束了。還有一些問題,好比綁定是否有優先順序?若是兩個規則都試圖將「this」綁定到同一個函數,這樣的衝突該怎麼辦?這是另外一篇文章的主題了。第3部分?可能吧,可是老實說,你不多會遇到這樣的衝突。因此如今咱們已經所有講完了,讓咱們總結一下咱們在這兩部分學到的東西。

總結

在第一部分中,咱們看到函數的「this」是如何變化的,而且如何根據函數的調用方式而改變。咱們討論了默認綁定規則,它適用於函數的獨立調用,而隱式綁定規則適用於調用函數時,前面有一個對象引用和箭頭函數,以及它們如何使用詞法綁定。在第一部分的結尾處,咱們還快速的介紹了在 JavaScript 對象中進行自調用。

在第二部分,咱們從 new 綁定開始,並討論它是如何工做以及如何可以輕鬆地破壞整個結構。這一部分的後半部分致力於使用 callapplybind 顯式地將'this'綁定到函數。我還略顯尷尬地與你分享了關於如何記住 callapply 之間差別的助記符。但願你能記住它。

這篇文章很長。很是感謝你能一直讀完。我但願這篇文章能讓你學到些東西。若是以爲還不錯,也請把這篇文章推薦給其餘人吧。祝你一天都有好心情!

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


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

相關文章
相關標籤/搜索