在 JavaScript 中有一個很特別、很常用又常常讓初學者很困擾的東西 ─ this,在這堂課中會來談談這個讓人又愛又恨的 this。
this 通常會指稱到一個物件,同時 this 會在不同的情境下指稱到不同的物件。 讓我們來看幾個不同的情境,幫助我們更了解 this。
Global Object (Window 物件)
這裡我們在三種不同情境去呼叫 this,分別是在程式的最外層(outer environment)直接去執行;使用function statement 去執行;使用 function expression 去執行(如果還不清楚 function statement 和 function expression 的差別,可以參考註1)。
// Outer Environment console.log(this); // function statement function thisInFunctionStatement() { console.log(this); } thisInFunctionStatement(); // function expression const thisInFunctionExpression = function() { console.log(this); } thisInFunctionExpression();
結果會發現,這三個 this 都會指稱到同樣的記憶體位置,也就是全域環境(global environment)中的 global object(在瀏覽器的環境下,global object 只的就是 Window 物件):
這也就是說,我們可以直接利用這個 function 和 this 在 Window 物件建立新的屬性:
function addNewPropertyInWindowObject() { console.log(this); this.newPropertyInWindow = 'Create a new property.' } addNewPropertyInWindowObject(); console.log(newPropertyInWindow); // Create a new property.
在這裡我們利用 this.newPropertyInWindow = "..." 來在 Window 物件中添加新的屬性,程式的最後,則可以直接 console.log(newPropertyInWindow),這裡之所以可以不用打 this.newPropertyInWindow 或 window.newPropertyInWindow 是因為任何在全域環境(global environment)下 Window 物件的屬性,都可以直接去指稱它,而不用使用 dot-notation( .)去指稱。
跑出來的結果會像這樣子:
透過 console.log(newPropertyInWindow) 會呼叫出該屬性的值- "Create a new property",同時,在Window 這個落落長的物件中,我們也會找到 newPropertyInWindow 這個屬性:
❗️在瀏覽器的執行環境下,global object 指的就是 Window 物件;在 NodeJS 的執行環境下 global object 則是指稱 Global 物件。
Method in object
我們知道,在物件裡的值如果是原生值(primitive type;例如,字串、數值、邏輯值),我們會把這個新建立的東西稱為「屬性(property)」;如果物件裡面的值是函式(function)的話,我們則會把這個新建立的東西稱為「方法(method)」。
在這裡,我們就要來建立物件中的方法:
const objectWithThis = { name: 'I am the object', log: function () { console.log(this); } } methodInObject.log();
首先,我們利用物件實體語法(object literal)的方式建立一個名為 objectWithThis 的物件(如果不清楚用「物件實體語法」來建立物件的方式,可以參考註 2),裡面包含了名為 name 的屬性和名為 log 的方法。其中 log 是一個 匿名函式(anonymous function),程式內容很簡單,就是呼叫出 this 而已(關於匿名函式可參考註 1)。最後則是使用 objectWithThis.log() 的方式來執行該方法。
猜猜看,這時候的 this 會是什麼呢? 答案是物件 objectWithThis!當這個函式是物件裡面的 method 時,這時候的 this 就會指稱到包含這個 method 的物件:
當某個函式是放在某一個物件裡面時,那麼該函式裡面的 this 指稱的就是該物件本身。
JavaScript 中關於 this 的一個問題
讓我們更進一步延伸來看這個範例:
const objectWithThis = { name: 'I am the object', log: function () { this.name = "Update current object name" // 修改 this.name 的名稱 console.log(this); } } objectWithThis.log();
假設我們在objectWithThis 物件內的 log 方法中多了一行 this.name = "Updated this Object name",因為我們知道 this 現在指的是物件 objectWithThis,所以可以想像的,當我執行這個 log 這個方法的時候,它就會去變更 objectWithThis.name 的值,所以:
這個部分是沒有什麼問題的。
但假設我在 log 方法裡面,另外建立一個函式叫做 setName,一樣是用 this.name = 'newName' 的方式來修改這個 objectWithThis 物件中 name 的屬性值時。最後再去呼叫 this 來看一下,也就是程式碼改為:
const objectWithThis = { name: 'I am the object', log: function () { this.name = 'Update current object name' console.log(this); const setNameWithFunction = function(newName) { this.name = newName; } setNameWithFunction('Update object name to "NEW NAME"'); console.log(this); } } objectWithThis.log();
你會發現結果 objectWithThis 中 name 屬性的值並沒有被再次修改,沒有被改成預期的 "Update object name to 'NEW NAME'":
怎麼一回事呢?
透過 console.log(window) 回頭仔細看一下 Window 物件,會發現在 Window 物件中發現了一個新的屬性 name,而且值是 "Update object name to 'NEW NAME'":
也就是說,原來我們剛剛在函式 setNameWithFunction 裡面的 this,指稱到的是 Window 物件(global object),而不再是剛剛名為 objectWithThis 的物件 !
我們可以在 setNameWithFunction 這個方法中,用 console.log(this) 來確認一下:
const objectWithThis = { name: 'I am the object', log: function () { this.name = 'Update current object name' console.log(this); const setNameWithFunction = function(newName) { this.name = newName; console.log(this); // 確認 this 指稱的對象 } setNameWithFunction('Update object name to "NEW NAME"'); console.log(this); } } objectWithThis.log();
結果如下,在 log 這個方法中,我們一共執行了三次的 console.log(this),第一個和第三個 this 指稱到的是物件 objectWithThis 沒錯,但第二個在 setNameWithFunction 中的 this,指稱到的卻是 Window 物件(global object),而這也就是為什麼 setNameWithFunction 這個方法沒辦法幫我們修改物件 objectWithThis 中 name 屬性的值的關係,因為 this 根本沒指稱到物件 objectWithThis。
而許多人認為,這是 JavaScript 長久以來存在的一個問題。
怎麼解決這個問題
那麼碰到上述這個例子時,我們可以怎麼做來避免指稱到不同的物件呢?
許多人的解法是這樣的,因為我們知道物件在指稱的時候是 by reference 的方式(不同的變數實際上是指稱到同一記憶體位置,可參考註 3),所以我們可以這樣做:
- STEP 1:在整個 function 的最上面加上一行 var self = this(有些人會用 var that = this)。由於 by reference 的特性,self 和 this 會指稱到同一個記憶體位置,而 this 指稱到的是原本預期該指稱到的物件 objectWithThis,所以 self 一樣會指稱到物件 objectWithThis 的記憶體位置。
- STEP 2: 接著,把方法 log 內原本使用的 this 都改成 self,這樣做可以確保 self 指稱到的是物件 objectWithThis 而不用擔心會像上面的例子一樣指稱到非預期的物件。
結果也如同我們預期的,在第二次 console.log(self) 的時候,就再次替換了物件 objectWithThis 中name 屬性的值。
總結
- 如果是在 global environment 建立函式並呼叫 this,這時候 this 會指稱到 global object(在瀏覽器的環境下就是 Window 物件)。
- 如果是在物件裡面建立函式,也就是方法(method)的情況時,這時候的 this 一般就會指稱到包含這個方法的物件(之所以說「一般」是因為除了上述「問題」的情況之外)。
- 碰到物件方法中可能會有不知道 this 指稱到什麼的情況時,為了避免不必要的錯誤,我們可以在該方法中的最上面建立一個變數,先去把原本的 this 保留下來(var self = this;)。
- 如果真的還是不知道那個情況下的 this 會指稱到什麼,就 console.log 出來看看會是最好的辦法!
如果真的還是不知道那個情況下的 this 會指稱到什麼,console.log 出來看會是最好的辦法!
程式範例
const objectWithThis = { name: 'I am the object', log: function () { var self = this; // 先透過 self 把原本的 this 存起來 self.name = 'Update current object name' const setNameWithFunction = function(newName) { self.name = newName; console.log(self); } setNameWithFunction('Update object name to "NEW NAME"'); console.log(self); } } objectWithThis.log();
延伸閱讀
- 👍 WTF is this - Understanding the this keyword, call, apply, and bind in JavaScript
- 重新認識 JavaScript: Day 20 What's "THIS" in JavaScript (鐵人精華版) @ iThome 鐵人賽 - 重新認識 JavaScript
備註
- 註1:[筆記] 進一步談 JavaScript 中函式的建立─function statements and function expressions
- 註2:[筆記] JavaScript 中的物件建立(Object)- Part 2
- 註3:[筆記] 談談 JavaScript 中 by reference 和 by value 的重要觀念
資料來源
- [Udemy] JavaScript: Understanding the Weird Parts- Objects, Functions, and 'This'
0 意見:
張貼留言