JavaScript 的 this 系列文終於來到最後一篇了,相信在前兩篇文章的說明下,各位對 this 應該有了基本的認識,而在這最後的篇幅中,我們將著重於 this 與前後文本 (context) 綁定的基本原則 且同時說明 如何決定 this 是誰的順序,期望各位在讀完這系列文章後,對於 this 所扮演的角色,能有更清楚、更深入的理解。

前情提要

這系列的主題其實是節錄自去年 (2016) 我在五倍紅寶石開設的課程,講的是 「This」 在 JavaScript 這門程式語言裡所代表的各種面貌。 然而最近無論是社群還是課堂教學,發現仍有不少剛入門的朋友對 JavaScript 的 This 代表的意義不太熟悉,那麼我想整理出這幾篇文章也許可以釐清你對 This 的誤解,反正資料也都還在,不如就整理出來與大家分享順便做個紀錄。

系列文快速連結:

工商服務

若是剛入門的朋友,覺得這系列內容較艱深難以消化,那麼 這裡 也有一門課程, 同樣是由我在五倍紅寶石為各位講解 JavaScript 與 jQuery 的入門課程,會比較偏向實際應用的開發。 預期在上完這門課程,看懂 this 這系列內容的能力應該是沒有問題,也歡迎有興趣的朋友報名。

this 與前後文本 (context) 綁定的基本原則

this 綁定的基本原則大致上可以分成下列四種:

  • 預設綁定 (Default Binding)
  • 隱含式綁定 (Implicit Binding)
  • 顯式綁定 (Explicit Binding)
  • 「new」關鍵字綁定


首先是 預設綁定 (Default Binding)

宣告在全域範疇 (global scope) 的變數,與同名的全域物件 (window 或 global) 的屬性是一樣的意思。 因為預設綁定的關係,當 function 是在普通、未經修飾的情況下被呼叫,也就是當 function 被呼叫的當下如果沒有值或是在 func.call(null)func.call(undefined) 此類的情況下,此時裡面的 this 會自動指定至全域物件

Copy Code

var a = 123;
console.log( window.a );

function foo(){
  // this === window
  console.log( this.a );
}

foo(); // 123

雖然預設綁定規則會將 function 中的 this 預設指向全域物件,但同樣的情況,若是加上 "use strict" 宣告成嚴格模式後,原本預設將 this 綁定至全域物件的行爲,會轉變成 undefined

Copy Code

var a = 123;
console.log( window.a );

function foo(){
  "use strict";
  // this === undefined
  console.log( this.a );
}

foo(); // TypeError

接著來看看 隱含式綁定 (Implicit Binding)

即使 function 被宣告的地方是在 global scope 中,只要它成為某個物件的參考屬性 (reference property),在那個 function 被呼叫的當下,該 function 即被那個物件所包含。

Copy Code

function func() {
  console.log( this.a );
}

var obj = {
  a: 2,
  foo: func
};

func();       // undefined
obj.foo();    // 2

在上面的範例中可以看到,根據 「預設綁定」的原則,直接呼叫 func() 的情況下,此時的 this.a 實際上會指向 window.a,所以結果是 undefined

而當我們在 obj 物件中,將 foo 這個屬性指到 func() 的時候,再透過 obj 來呼叫 obj.foo() 的時候,雖然實際上仍是 func() 被呼叫, 但此時的 this 就會指向至 obj 這個 owner 的物件上,於是此時的 this.a 就會是 obj.a 也就是 2 。

理解了隱含式綁定的原則後,繼續來看看這個變化過的版本:

Copy Code

function func() {
  console.log( this.a );
}

var obj = {
  a: 2,
  foo: func
};

obj.foo();  // 2

var func2 = obj.foo;
func2();    // ??

在稍早的說明中,我們已經知道 obj.foo() 的結果會是 2。 此時,我們宣告另一個變數 func2 指向 obj.foo,那麼聰明的你是否可以猜到呼叫 func2() 的結果為何呢?

.
.
.
.
.
.

好,答案揭曉,是 undefined

先別急著翻桌,雖然 func2 看起來是對 obj.foo 的參考,但實際上 func2 參考的對象是 window.func

為什麼?

在本系列上篇的時候,我們曾經說過,全域變數的上層物件是誰? 就是 window。 所以說宣告 var func2 = obj.foo; 的時候,實際上 func2 就是 window.func2,而你在執行 func2() 的時候,等同於執行 window.func2(),那麼此時的 this 就會是 window,而 this.a 自然就會是 undefined

不信的話,你可以再宣告一個全域變數 a 並給定某個值之後再試著執行一次 func2() 來應證看看。

換句話說,決定 this 的關鍵不在於它屬於哪個物件,而是在於 function「呼叫的時機點」,當你透過物件呼叫某個方法 (method) 的時候,此時 this 就是那個物件 (owner object)。


再來是顯式綁定 (Explicit Binding)

相較於前兩種,顯式綁定就單純許多, 簡單來說就是透過 .bind() / .call() / .apply() 這類直接指定 this 的 function 都可被歸類至顯式綁定的類型。

像這樣:

Copy Code

function func() {
  console.log( this.a );
}

var obj = {
  a: 2
};

func();             // undefined
func.call(obj);     // 2

關於 .bind() / .call() / .apply() 的介紹在本系列的 中篇 已有不少篇幅說明,有興趣的朋友可直接點擊閱讀。

這裏值得一提的是,相信大家都已經知道,在巢狀函式的情況下 this 的結果會因為內部找不到 owner object 而被指向至 window, 過去我們可以透過宣告另一個變數 (thatself 等) 來暫存指向原本的 this

那麼,這裡要來介紹另一個小技巧「Hard binding」,透過 .call() 包裝,使得 function 不管在哪裡執行, 其中的 this 都可以保持在我們所指定的那個物件上:

Copy Code

function func() {
  console.log( this.a );
}

var obj = {
  a: 2
};

var obj2 = {
  a: 100
};

// Hard binding function
var hard_binding_func = function() {
  func.call( obj );
};

func();                 // undefined
func.call(obj);         // 2
func.call(obj2);        // 100

hard_binding_func();    // 2
hard_binding_func.call(obj2);  // 2

window.setTimeout( func, 10);  // undefined
window.setTimeout( hard_binding_func, 10);  // 2

在上面的範例中,我們透過一個 hard_binding_func 的 function 來把 func.call( obj ) 包裝起來, 於是此時不管 hard_binding_func 在哪裡執行,都不再受外層的 this 變化所影響,因為包裝後 functhis 已經被鎖定在 obj 上了, 所以始終都是維持在 obj.a 也就是 2 的結果,這類的處理技巧就叫做「Hard binding」。

當然,自從 ES5 有了 .bind() 之後,剛剛的寫法甚至可以寫成這樣:

Copy Code

// Hard binding function
var hard_binding_func = func.bind(obj);

執行的結果都是一樣的。

最後一個是「new」關鍵字綁定

在傳統類別導向 (class-oriented) 的程式語言中,建構子 (constructors) 是被附接到類別上的特殊方法,在透過 new 將 class 實體化的時候,這個建構子方法就會被呼叫。 而 JavaScript 雖然也有 new 這個關鍵字,運作時也與類別導向的語言行為類似,但由於 JavaScript 並不是一個類別導向的程式語言,所以它的 new 運作原理並不相同。

當一個 function 前面帶有 new 被呼叫時,會發生:

  • 會產生一個新的物件 (物件被建構出來)
  • 這個新建構的物件會被設為那個 function 的 this 綁定目標,也就是 this 會指向新建構的物件。
  • 除非這個 function 指定回傳 (return) 了他自己的替代物件,否則這個透過 new 產生的物件會被自動回傳。

Copy Code

function foo(a) {
  this.a = a;
}

var obj = new foo( 123 );
console.log( obj.a );      // 123

在上面的範例中,因為呼叫 foo 時,加了一個 new ,所以建構了一個新物件,並回傳到 obj。 透過傳入的參數 123,在建立物件的時候,會作為新物件的屬性 a 的值,這種用 new 建立 this 綁定的方式,就是 new 關鍵字綁定的方式。

this 綁定的優先順序

看完了 this 與前後文本 (context) 綁定的基本原則後,接著我們來看看 this 綁定的優先順序。

當「隱含式綁定」與「顯式綁定」衝突時,此時 this 會以「顯式綁定」為主:

Copy Code

function func() {
  console.log( this.a );
}

var obj1 = { a: 2, foo: func };

var obj2 = { a: 3, foo: func };

// 隱含式綁定
obj1.foo();  // 2
obj2.foo();  // 3

// 顯式綁定
obj1.foo.call( obj2 );  // 3
obj2.foo.call( obj1 );  // 2
function func(something) {
  this.a = something;
}

var obj1 = { foo: func };
var obj2 = {};

// 以參數帶入,則 this 為物件本身
obj1.foo( 2 );
console.log( obj1.a );    // 2

// 透過 call 強至指定 this
obj1.foo.call( obj2, 3 );
console.log( obj2.a );    // 3

// this = 建構子所產生的物件
var bar = new obj1.foo( 4 );
console.log( obj1.a );    // 2
console.log( bar.a );     // 4

總結

綜合上述範例介紹,我們可以簡單總結出一個結論:

  • 這個 function 的呼叫,是透過 new 進行的嗎? 如果是,那 this 就是被建構出來的物件。
  • 這個 function 是以 .call().apply() 的方式呼叫的嗎? 或是 function 透過 .bind() 指定? 如果是,那 this 就是被指定的物件。
  • 這個 function 被呼叫時,是否存在於某個物件? 如果是,那 this 就是那個物件。
  • 如果沒有滿足以上條件,則此 function 裡的 this 就一定是全域物件: window 或是 global,在嚴格模式下則是 undefined

而決定 this 是誰的關鍵:

  • function 可以透過 .bind() 來指定 this 是誰。
  • 當 function 透過 call()apply() 來呼叫時, this 會指向第一個參數,且會立即被執行。
  • callback function 內的 this 會指向呼叫 callback function 的物件。
  • ES6 箭頭函數內建 .bind() 特性,此時 this 無法複寫。

關於 this 的介紹就在此告一段落。 相信在看完這系列關於 this 的介紹後,對於 this 在 JavaScript 這個程式語言所扮演的角色,能有更清楚、更深入的理解,針對系列文中若有任何疑問,也歡迎留言討論以及指正。

也希望這系列文章對還在學習 JavaScript 的你有所幫助。

最後再來小小工商一次,我在 五倍紅寶石 所開設的 JavaScript & jQuery 前端開發入門實戰 這門課程仍然持續開放報名中,不管你是想入門 JavaScript、對 jQuery 的開發應用有興趣,或是想要更進一步了解 JavaScript 以及 jQuery 在網站前端開發的技巧,都歡迎你來報名。 :)