Photo by Juanjo Jaramillo on Unsplash
在傳統開發的過程中,很容易會搞混一般的 this 和箭頭函式(arrow function)中的 lexcial "this" 兩者的差異。在傳統函式中,this 是在函式調用時動態決定,換句話說只要是哪一個物件呼叫函式,他就會抓取該物件進行呼叫;而箭頭函式則是在定義時就決定,並繼承自父層的作用域。
傳統的 this 定義
我們直接以下列的案例來說明。我們實例化一個物件 person,並設定一個 greet 的方法,其中使用 SetTimeout 的方法來執行。
const person = {
name: 'Chris',
greet: function() {
setTimeout(function() {
console.log(`Hello, my name is ${this.name}`);
}, 1000);
}
};
person.greet(); // Hello, my name is undefined (after 1 second)
在上面的這個例子中,因為 setTimeout 並非 JavaScript 內建的函式,而是屬於 Web API,因此在執行 SetTimeout 後會先將 Timer 放進 Web API 等待呼叫。
這時就會先在 Web API 確定是否一秒後,確認一秒後會執行 Web API 的 Timeout,將 Timeout 放進 task Quene 移到 Stack 中執行。此時在全域環境(Stack)確認後,並沒有待執行的內容。
接著 Timeout 函式就會被放進 Stack 中,Timeout 就會呼叫 Callback 來執行。此時,執行的環境就會是 window 而非此處的 person。因此結果就會是 "Hello, my name is undefined"。
除非特別使用 Call 或 Apply 來綁定呼叫的物件,才能讓這個 greet 順利運行。以下是實際範例:
const person = {
name: 'Chris',
greet: function() {
setTimeout(function() {
console.log(`Hello, my name is ${this.name}`);
}.apply(this), 1000);
}
};
person.greet(); // Hello, my name is Chris (after 1 second)
透過 Apply 強制將此處的 this(就是 person)綁定給 setTimeout 的 callback function,才在執行 greet 時,順利顯示正確的 Chris。
箭頭函式中的 lexcial this 定義
當初在理解父層 this 的環境定義時,時常不確定指的到底是哪一個環境。但這裡只要簡單的記得:
箭頭函式的 this 指的是定義該 function 的環境(lexical Scope)中,所使用的 this。
我們一樣以上面的例子說明:
const person = {
name: 'Chris',
greet: function() {
setTimeout(() => {
console.log(`Hello, my name is ${this.name}`);
}, 1000);
}
};
person.greet(); // Hello, my name is Chris
雖然上述的 setTimeout 創建了一個執行環境(Execution context),但因為箭頭函式的 this 是 lexical bound。
可以看到此處創建箭頭函式的環境,是被宣告在 greet 這個 function 裡,所以 greet 就是這個箭頭函式的 Lexical Scope。因此此處 greet 的 this,就是箭頭函式的 this。因此,此處仍然會顯示 Chris。
這裡也舉另一個例子,如果 greet 不要使用一個 function 包裹,那上層的 greet 裡使用 this 一樣會抓到 window。因為在定義的當下,再往上一層的 Scope Chain 會抓到 window,因此此處的 this 就會是 window。
因此若要在方法使用箭頭函式,則需使用傳統函式包裹,才能順利使用。
如何理解執行上下文(Execution Context)?
一般來說,總共有五中 Execution Context 會生成:
- 全域執行上下文(Global Execution Context):全域執行上下文是在程式開始執行時創建的,代表整個程式的執行環境。在全域執行上下文中,會創建全域物件(global object)和全域範疇(global scope)。
- 函式執行上下文(Function Execution Context):每當函式被呼叫時,都會創建一個新的函式執行上下文。每個函式執行上下文都有自己的範疇,包含函式內部的變數、參數、函式內部定義的其他函式等。
- Eval 函式執行上下文(Eval Function Execution Context):當程式碼被 eval() 函式執行時,會創建一個 Eval 函式執行上下文。Eval 函式執行上下文允許執行字串中的程式碼,但在現代 JavaScript 中,建議盡量避免使用 eval() 函式。
- 模組執行上下文(Module Execution Context):在使用 ES6 模組的情況下,每個模組都會有自己的模組執行上下文,模組內的變數、函式等僅在模組內可見,預設不會暴露到全域範疇。
- 物件執行上下文(Object Execution Context):當函式作為物件的方法被呼叫時,該函式執行上下文被稱為物件執行上下文。在物件執行上下文中,物件本身被設置為 this,並且可以訪問該物件的屬性和方法。
在理解箭頭函式的 this 時,最容易被搞混的原因,其實是來自於上述的「物件執行上下文」與「函式執行上下文」,兩者的認識不夠清楚。
箭頭函式的 this 如何參考?
一般來說,當一個傳統函式作為某物件的執行方法時,就會創建物件執行上下文。因此,在方法中使用 this,就會指向該物件本身。
然而,將箭頭函式使用在物件方法時,因為本身箭頭函式不會建立物件執行上下文,因為他本身並沒有 this。相反地,箭頭函式僅參考外層範疇(lexical Scope)環境中的 this、argument。除此之外,箭頭函式也沒有自己的建構函式。
換句話說,如果直接在全域中物件呼叫 {key: this},該 this 會指向全域物件,因為此處的 this 仍然屬於全域執行上下文,因此 this 依然會顯示 window,或是嚴格模式下的 undefined。
因此,只要簡單瞭解:
箭頭函式本身並不會建立物件上下文,而是僅會參考外層環境(Lexical Scope)的 this,建立函式上下文而已。
瞭解 this 在 JavaScript 中扮演的角色
原先 this 綁定執行的物件,是為了呼叫時的方便;但隨著開發的複雜、物件繼承的情境,甚至非同步的問題變得複雜,也因此才出現了箭頭函式。
但箭頭函式並非要取代傳統函式,因為箭頭函式本身並沒有自己的 this 可以使用,單純是綁定外層的父環境,因此在 JavaScript 後續開發的過程,主要扮演了互補的角色。
參考資料