2016年3月24日 星期四

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

圖片來源:Udemy

在JavaScript中有一個很特別、很常用又常常讓初學者很困擾的東西 ─ "this",在這堂課中會來談談這個"this"。


this通常會指稱到一個物件,同時this會在不同的情境下指稱到不同的物件。

讓我們來看幾個不同的情境,幫助我們更了解"this"。

window object (global object)


這裡我們在三種不同情境去呼叫"this",分別是在程式的最外層(outer environment)直接去執行;使用function statement去執行;使用function expression去執行(如果還不清楚function statement和function expression的差別,可以參考註1)。


結果會發現,這三個"this"都會指稱到同樣的記憶體位置,也就是global environment的window object (global object):


這也就是說,我們可以直接利用這個function和this在window object建立新的屬性:

在這裡我們利用this.NewVariable = "..."來在window object建立新的屬性,程式的最後,我們則可以直接console.log(NewVariable),這裡之所以可以不用打this.NewVariablewindow.NewVariable是因為任何在global object (window)的屬性,我們都可以直接去指稱它,而不用使用"."


跑出來的結果會像這樣子:


它會呼叫出我們的"Create a new property",同時,在window這個落落長的object中,我們也會找到NewVariable這個屬性:

method in object


我們知道,在物件裡的值如果是原生值(primitive type;例如,字串、數值、邏輯值),我們會把這個新建立的東西稱為「屬性(property)」;如果物件裡面的值是函式(function)的話,我們則會把這個新建立的東西稱為「方法(method)」。

在這裡,我們就要來建立method:

首先,我們利用object literal的方式建立一個物件c(如果不清楚用object literal來建立物件的方式,可以參考註2),裡面包含屬性name和方法log。log是一個匿名函式(anonymous function),程式內容很簡單,就是呼叫出this而已(關於匿名函式可參考註1)。最後則是使用c.log的方式來執行該方法。


讓我們來看看,這時候的"this"會是什麼呢?

答案是物件c!

當這個函式是物件裡面的method時,這時候的this就會指稱到包含這個method的物件

JavaScript中關於this的一個bug


讓我們更進一步延伸來看這個範例:

假設我們在method log裡面多這一行this.name = "Updated Object C name"


因為我們知道"this"現在指的是物件c,所以可以想像的,當我執行這個method的時候,它會去變更c.name的值。


這個部分是沒有什麼大問題的,不過讓我們繼續看下去......。

假設我在method log裡面在做一些變更,我在這個method裡面,另外建立一個函式叫做setname,一樣是用this.name = newname的方式來修改這個object c中name屬性的值。

接著執行setname這個function,希望把object c中name的屬性值改成"New name for object c",最後再去呼叫"this"來看一下。


結果我們會發現,Object C中name屬性的值並沒有變成"New name for object c",竟然還是一樣!?怎麼會這樣呢?


仔細一看,我們回來看一下我們的window object,我們會發現,在window object中發現了一個新的屬性"name",而且值是"New name for object c"。


這是什麼意思呢?意思是原來我們剛剛在函式setname裡面的this,指稱到的是global object (window object),而不再是剛剛的object C


我們在setname這個function中,用console.log(this)來看一下:


在log這個method中,我們一共執行了三次的console.log(this)結果如下:

第一個和第三個"this"指稱到的是物件C,而第二個在setname中的this,指稱到的則是window object (global object),而這也就是為什麼setname這個function沒辦法幫我們修改物件c中name屬性的名稱,因為"this"根本沒指稱到物件c。


而許多人都認為,這是JavaScript的一個bug。

那麼我們可以怎麼做


那麼碰到上述的這個例子時,我們可以怎麼做來避免指稱到不同的物件呢?

許多人的解法是這樣的,

因為我們知道物件在指稱的時候是by reference的方式(不同的變數實際上是指稱到同一記憶體位置,可參考註3),所以我們可以這樣做

STEP 1
我們在整個function的最上面加上一行var self = this(有些人會用var that = this)。由於by reference的特性,self和this會指稱到同一個記憶體位置,而this指稱到的是object C,所以self一樣會指稱到object c的記憶體位置。

STEP 2

接著,把方法log內原本使用的"this"都改成"self",這樣做可以確保self指稱到的是物件c而不用擔心會像上面的例子一樣指稱到錯誤的物件。


結果也如同我們預期的,在第二次console.log(self)的時候,就再次替換了物件C中name屬性的值。


總結


讓我們來總結一下:

1. 如果我們是在global environment建立函式並呼叫this,這時候this會指稱到global object (window object)。

2. 如果我們是在物件裡面建立函式,也就是方法(method)的情況時,這時候的this一般就會指稱到包含這個方法的物件(之所以說"一般"是因為除了上述bug的情況之外)。

3. 碰到method中可能會有不知道this指稱到什麼的情況時,為了避免不必要的錯誤,我們可以在method中的最上面建立一個變數,去把它指定成this(var self = this)。

4.如果真的還是不知道那個情況下的this會指稱到什麼,就console.log出來看看吧!

程式範例


// function statement

function a(){
 console.log(this);
 this.NewVariable = "Create a new property";
}

a();

console.log(NewVariable);



var c = {
 name:"The C object",
 log: function(){
  
  var self = this;

  self.name = "Updated object C name";
  console.log(self);
  
  var setname = function(newname){
   self.name = newname;
   console.log(self);
  }
  setname("New name for object c");
  console.log(self)

 }
}

c.log();
→回到此系列文章目錄

資料來源

[Udemy] JavaScript: Understanding the Weird Parts- Objects, Functions, and 'This'

延伸閱讀



Share:

0 意見:

張貼留言