Ch. 7 this 現在全都說得通了!


呼叫地點

尋找呼叫地點時需要考慮到呼叫堆疊 (call-stack)。
觀察或追蹤容易出錯,可以利用開發者工具協助尋找。


不過就是規則

預設繫結

當呼叫是普通的參考,this 會指向全域。因此 this.avar 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 被調用時,以下事情會發生:

  1. 一個無中生有的全新物件被建構出來。
  2. 新建構的物件會帶有 [[Prototype]] 連結。
  3. 這個新建構的物件會被設為那個函式呼叫的 this 繫結。
  4. 除非該函式回傳了它自己提供的替代物件,不然這個函式會自動回傳這個新建構的物件。
function foo(a) {
	this.a = a;
}
var bar = new foo( 2 );
console.log( bar.a ); // 2

一切都按順序來

四種規則的優先順序如下:

  1. new 繫結:該函式呼叫以 new 進行。
  2. 明確繫結:該函式透過 call(..)apply(..)bind(..) 進行呼叫。
  3. 隱含繫結:該函式以一個情境 (擁有物件或包含物件) 被呼叫。
  4. 若前面規則都不是用,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 繫結。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *