2016年3月24日

[筆記] 談談 JavaScript 中的 "this" 和它的問題

img
在 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 物件):
Imgur
這也就是說,我們可以直接利用這個 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.newPropertyInWindowwindow.newPropertyInWindow 是因為任何在全域環境(global environment)下 Window 物件的屬性,都可以直接去指稱它,而不用使用 dot-notation( .)去指稱。
跑出來的結果會像這樣子:
Imgur
透過 console.log(newPropertyInWindow) 會呼叫出該屬性的值- "Create a new property",同時,在Window 這個落落長的物件中,我們也會找到 newPropertyInWindow 這個屬性:
Imgur
❗️在瀏覽器的執行環境下,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 的物件
Imgur
當某個函式是放在某一個物件裡面時,那麼該函式裡面的 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 的值,所以:
Imgur
這個部分是沒有什麼問題的。
但假設我在 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();
你會發現結果 objectWithThisname 屬性的值並沒有被再次修改,沒有被改成預期的 "Update object name to 'NEW NAME'":
Imgur
怎麼一回事呢?
透過 console.log(window) 回頭仔細看一下 Window 物件,會發現在 Window 物件中發現了一個新的屬性 name,而且值是 "Update object name to 'NEW NAME'":
Imgur
也就是說,原來我們剛剛在函式 setNameWithFunction 裡面的 this,指稱到的是 Window 物件(global object),而不再是剛剛名為 objectWithThis 的物件 !
Imgur
我們可以在 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 這個方法沒辦法幫我們修改物件 objectWithThisname 屬性的值的關係,因為 this 根本沒指稱到物件 objectWithThis
Imgur
而許多人認為,這是 JavaScript 長久以來存在的一個問題。

怎麼解決這個問題

那麼碰到上述這個例子時,我們可以怎麼做來避免指稱到不同的物件呢?
許多人的解法是這樣的,因為我們知道物件在指稱的時候是 by reference 的方式(不同的變數實際上是指稱到同一記憶體位置,可參考註 3),所以我們可以這樣做:
  • STEP 1:在整個 function 的最上面加上一行 var self = this(有些人會用 var that = this)。由於 by reference 的特性,selfthis 會指稱到同一個記憶體位置,而 this 指稱到的是原本預期該指稱到的物件 objectWithThis,所以 self 一樣會指稱到物件 objectWithThis 的記憶體位置。
  • STEP 2: 接著,把方法 log 內原本使用的 this 都改成 self,這樣做可以確保 self 指稱到的是物件 objectWithThis 而不用擔心會像上面的例子一樣指稱到非預期的物件。
Imgur
結果也如同我們預期的,在第二次 console.log(self) 的時候,就再次替換了物件 objectWithThisname 屬性的值。
Imgur

總結

  1. 如果是在 global environment 建立函式並呼叫 this,這時候 this 會指稱到 global object(在瀏覽器的環境下就是 Window 物件)。
  2. 如果是在物件裡面建立函式,也就是方法(method)的情況時,這時候的 this 一般就會指稱到包含這個方法的物件(之所以說「一般」是因為除了上述「問題」的情況之外)。
  3. 碰到物件方法中可能會有不知道 this 指稱到什麼的情況時,為了避免不必要的錯誤,我們可以在該方法中的最上面建立一個變數,先去把原本的 this 保留下來(var self = this;)。
  4. 如果真的還是不知道那個情況下的 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();

延伸閱讀

備註

資料來源

0 意見:

張貼留言