What's THIS in JavaScript ? [下]
JavaScript 的 this 系列文終於來到最後一篇了,相信在前兩篇文章的說明下,各位對 this
應該有了基本的認識,而在這最後的篇幅中,我們將著重於 this 與前後文本 (context) 綁定的基本原則 且同時說明 如何決定 this 是誰的順序,期望各位在讀完這系列文章後,對於 this
所扮演的角色,能有更清楚、更深入的理解。
前情提要
這系列的主題其實是節錄自去年 (2016) 我在五倍紅寶石開設的課程,講的是 「This」 在 JavaScript 這門程式語言裡所代表的各種面貌。 然而最近無論是社群還是課堂教學,發現仍有不少剛入門的朋友對 JavaScript 的 This
代表的意義不太熟悉,那麼我想整理出這幾篇文章也許可以釐清你對 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 會自動指定至全域物件。
var a = 123;
console.log( window.a );
function foo(){
// this === window
console.log( this.a );
}
foo(); // 123
雖然預設綁定規則會將 function 中的 this 預設指向全域物件,但同樣的情況,若是加上 "use strict"
宣告成嚴格模式後,原本預設將 this
綁定至全域物件的行爲,會轉變成 undefined
。
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 即被那個物件所包含。
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 。
理解了隱含式綁定的原則後,繼續來看看這個變化過的版本:
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 都可被歸類至顯式綁定的類型。
像這樣:
function func() {
console.log( this.a );
}
var obj = {
a: 2
};
func(); // undefined
func.call(obj); // 2
關於 .bind()
/ .call()
/ .apply()
的介紹在本系列的 中篇 已有不少篇幅說明,有興趣的朋友可直接點擊閱讀。
這裏值得一提的是,相信大家都已經知道,在巢狀函式的情況下 this
的結果會因為內部找不到 owner object 而被指向至 window
,
過去我們可以透過宣告另一個變數 (that
或 self
等) 來暫存指向原本的 this
。
那麼,這裡要來介紹另一個小技巧「Hard binding」,透過 .call()
包裝,使得 function 不管在哪裡執行,
其中的 this
都可以保持在我們所指定的那個物件上:
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
變化所影響,因為包裝後 func
的 this
已經被鎖定在 obj
上了,
所以始終都是維持在 obj.a
也就是 2 的結果,這類的處理技巧就叫做「Hard binding」。
當然,自從 ES5 有了 .bind()
之後,剛剛的寫法甚至可以寫成這樣:
// 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
產生的物件會被自動回傳。
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
會以「顯式綁定」為主:
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 在網站前端開發的技巧,都歡迎你來報名。 :)