呼叫地點
尋找呼叫地點時需要考慮到呼叫堆疊 (call-stack)。
觀察或追蹤容易出錯,可以利用開發者工具協助尋找。
不過就是規則
預設繫結
當呼叫是普通的參考,this 會指向全域。因此 this.a
與 var a = 2;
,是相同的 a。
在嚴格模式中,全域物件不會是預設繫結, this 會被改為 undefined。
當 function foo()
宣告不處於嚴格模式中,仍會套用預設繫結。
隱含的繫結
呼叫地點有情境物件 (context object) 時,該物件被作為 this 的參考。
obj 在函式被呼叫的那時「擁有 (owns)」或「包含 (contains)」函式參考。
在參考串鏈中,只有最終層才對呼叫有意義。
function foo() { console.log( this.a ); } var obj2 = { a: 42, foo: foo }; var obj1 = { a: 2, obj2: obj2 }; obj1.obj2.foo(); // 42
隱含地失去
當函式失去了繫結的時候,代表它會退回到預設繫結。
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // function reference/alias! var a = "oops, global"; // `a` also property on global object bar(); // "oops, global"
bar()
是真正的呼叫地點,其參考到的只是一個函式的位址,而且是普通、未經修飾的呼叫,因此 this 退回到了預設繫結。
callback 經常會失去 this 繫結,因為它們僅僅是對函式本身的一個參考。而有些套件或函式庫可能會擅自修改 this 的指向。
明確的繫結
使用 call(..)
、apply(..)
能夠明確地指定 this 的繫結。
硬繫結 (hard binding)
但這仍無法避免隱含地失去或是繫結被套件蓋過的情況,要避免這類情況,可以採用一個明確繫結的變體-硬繫結,在函式的內部手動呼叫 call(..)
,強制以 obj 作為 foo 的 this 參考。如此一來,不論之後如何調用,最終都會以 obj 調用 foo。
function foo() { console.log( this.a ); } var obj = { a: 2 }; var bar = function() { foo.call( obj ); }; bar(); // 2 setTimeout( bar, 100 ); // 2 // `bar` hard binds `foo`'s `this` to `obj` // so that it cannot be overriden bar.call( window ); // 2
ES5 中提供了 bind(..)
,回傳一個新的函式,其中寫定所指定的 this 情境物件來呼叫函式。
API 呼叫的「情境」
許多函式或一些原生內建函式,有提供選擇性的參數,可以傳遞 context 作為 this 的參考。如 Array.forEach()
、Array.map()
。
想想這類的使用情境?
new 繫結
在 JS 中,建構器 (constructor) 只是前面接著 new
運算子被呼叫的函式,而這使得函式呼叫變成了一個建構器呼叫。它們只是正規的函式,但調用時,其行為實際上會被前面使用的 new
所接管。
當函式前面帶有 new
被調用時,以下事情會發生:
- 一個無中生有的全新物件被建構出來。
- 新建構的物件會帶有 [[Prototype]] 連結。
- 這個新建構的物件會被設為那個函式呼叫的 this 繫結。
- 除非該函式回傳了它自己提供的替代物件,不然這個函式會自動回傳這個新建構的物件。
function foo(a) { this.a = a; } var bar = new foo( 2 ); console.log( bar.a ); // 2
一切都按順序來
四種規則的優先順序如下:
- new 繫結:該函式呼叫以
new
進行。 - 明確繫結:該函式透過
call(..)
、apply(..)
、bind(..)
進行呼叫。 - 隱含繫結:該函式以一個情境 (擁有物件或包含物件) 被呼叫。
- 若前面規則都不是用,this 就是預設值,global 物件,若是嚴格模式中則為 undefined。
繫結的例外
忽略 this
傳入 null 或 undefined 作為 call(..)
、apply(..)
、bind(..)
的 this 參數,會轉而套用預設繫結。
看起來很反常,但有時會使用 apply(..)
來攤開陣列作為函式呼叫的參數,或是使用 bind(..)
來 curry 參數。
但使用 null 可能會有風險,特別是在使用第三方函式庫的狀況,可能會不經意地參考或修改到 global 物件。因此安全的做法是傳遞一個空物件,並且使用特殊符號 (如 ø ) 來作為辨識。
間接參考
function foo() { console.log( this.a ); } var a = 2; var o = { a: 3, foo: foo }; var p = { a: 4 }; o.foo(); // 3 (p.foo = o.foo)(); // 2
間接參考實際上只是指向到底層的函式物件,所以實際上只是呼叫 foo()
。
軟化繫結
可以撰寫一個軟繫結工具,為預設繫結提供一個非 global / undefined 的預設值,同時讓函式能夠經由隱含繫結或明確繫結手動給定 this 繫結。
語彙的 this
箭頭函式不使用前面四項規則,它的 this 繫結取自包含它的範疇。同時,箭頭函式的語彙繫結無法被覆寫。在 callback 函式中容易使用到。
function foo() { setTimeout(() => { // `this` here is lexically adopted from `foo()` console.log( this.a ); },100); } var obj = { a: 2 }; foo.call( obj ); // 2
值得注意的是,箭頭函式基本上使得傳統的 this 機制消失了,而是採用語彙範疇。箭頭函式雖然看起來吸引人,但它與 this 是不同風格和概念的用法,要盡可能避免混合使用。
複習
- 判斷 this 的四種規則和優先順序。
- 要小心並避免觸發預設繫結規則,若要安全地忽略 this 繫結,可以採用空物件來佔位,減少副作用。
- ES6 的箭頭函式是使用語彙範疇來處理 this 繫結,會從包含它們的函式呼叫繼承 this 繫結。