Understand JavaScript #12 物件 × 函式 × this
本文主要內容為探討物件、函式,以及那個令人困惑的「this」的指向問題與相關知識。
暖身時間
本文會使用到執行環境、變數環境、範圍鏈 ⋯⋯ 等觀念。
在開始之前,我們回味一下之前的東西。
- 程式執行:當函式物件的 Code 屬性被 Invoke 時,執行環境會被創造,並放進執行堆
- 範圍鏈:函式物件裡的變數有變數環境,它可以參考到外部(詞彙)環境,並一路隨著範圍鏈向外尋找,直到全域環境為止
this 指向全域物件 Window
每當執行環境被創造,JavaScript 引擎都會產生 this
變數給我們,它會指向不同物件,會依據該「函式如何被呼叫」來決定(改變)。
下面有三個執行環境(全域、呼叫 a 創造的、呼叫 b 創造的),每一種情況中,他們都有自己的 this
關鍵字,但這三個 this
都指向同一個記憶體位址的物件,也就是全域物件 window
。
1console.log(this); // Window {…} 2 3// Function Statement 4function a() { 5 console.log(this); // Window {…} 6} 7a(); 8 9// Function Expression 10var b = function () { 11 console.log(this); // Window {…} 12}; 13b(); 14
我們可以透過點運算子連結一個新的變數到全域物件,而任何在全域物件下的變數,我們可以直接參考到它,不需要透過點運算子。
1function a() { 2 console.log(this); 3 this.newVariable = 'Hello'; 4} 5a(); 6console.log(newVariable); // Hello 7
this 指向包含該方法的物件
在物件中,如果一個屬性的值是純值,我們會稱之為「屬性」,但如果一個屬性的值是一個函式,我們會稱之為「方法」,如下方範例所示。
1var c = { 2 // 屬性 (Property) 3 name: 'The c object', 4 5 // 方法 (Method) 6 log: function () { 7 console.log(this); 8 }, 9}; 10 11c.log(); // {name: "The c object", log: ƒ} 12
當我們呼叫的函式是物件的方法時,關鍵字 this
會指向「包含這個方法的物件」。
在這個範例中 this
會指向 c 物件,所以我們可以在 log
方法中,使用 this.name
去改變 c 物件 name
屬性的值。
1var c = { 2 name: 'The c object', 3 log: function () { 4 this.name = 'Updated c object'; 5 console.log(this); 6 }, 7}; 8 9c.log(); // {name: "Updated c object", log: ƒ} 10
陷阱!JavaScript 設計上的小缺陷
設計缺陷範例
如果在物件 c 的 log
方法裡面創造一個 setName
函式,試著用 this.name = newName
去改變 name 屬性的值。
根據剛才的說法,這個 setName
函式的 this 會指向包含該函式的物件(也就是 c 物件),導致 c 物件中的 name
屬性被改成 Updated again! The c object
。
但是結果卻不如預期 🤔
1var c = { 2 name: 'The c object', 3 log: function () { 4 this.name = 'Updated c object'; 5 console.log(this); // {name: "Updated c object", log: ƒ} 6 7 var setName = function (newName) { 8 this.name = newName; 9 }; 10 setName('Updated again! The c object'); 11 console.log(this); // {name: "Updated c object", log: ƒ} 12 }, 13}; 14 15c.log(); 16
經過一番折騰後,我們在全域物件 window
裡面找到了剛才的 name
屬性,而且它的值為 "Updated again! The c object"
,也就是說剛才等號運算子新增到了 window
裡面,也就代表著 this
是指向全域物件 window
而非 c 物件。
不怪你,這真的就是 JavaScript 設計上的錯誤或缺陷。但是我們該如何解決這個問題,如何讓 this 指向正確的物件呢?
常用解決方法
有一個常用的方法可以應付這個情況。
我們都知道物件是用 By Reference 設定的,而且函式第一層的 this
沒有設計缺陷,所以我們通常會在方法的第一行設定一個變數 self
等於 this
,讓這個變數 self
指向正確的物件。
當子函式發現 self
就會依據範圍鏈向外尋找,然後找到方法第一層中被設定為 this
的 self
。
往後如果在子函式裡面需要用到 this
指向該物件,就一律使用 self
來處理就好哩 👍
1var c = { 2 name: 'The c object', 3 log: function () { 4 var self = this; 5 6 self.name = 'Updated c object'; 7 console.log(self); // {name: "Updated c object", log: ƒ} 8 9 var setName = function (newName) { 10 self.name = newName; 11 }; 12 setName('Updated again! The c object'); 13 console.log(self); // {name: "Updated again! The c object", log: ƒ} 14 }, 15}; 16 17c.log(); 18
補充:使用 ES6 的 let 關鍵字也可以解決這樣的問題喔。
回顧
看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…
- 不同情況下
this
的指向 - 在物件的函式中透過設定
self
為this
來解決指向問題 - 沒有什麼程式語言是完美的,雖然 JavaScript 有設計上的缺陷,但是我們可以透過一些方法去彌補缺陷的存在