特別說(shuō)明,為便于查閱,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS
在第一章中,我們摒棄了種種對(duì) this
的誤解,并且知道了 this
是一個(gè)完全根據(jù)調(diào)用點(diǎn)(函數(shù)是如何被調(diào)用的)而為每次函數(shù)調(diào)用建立的綁定。
調(diào)用點(diǎn)(Call-site)
為了理解 this
綁定,我們不得不理解調(diào)用點(diǎn):函數(shù)在代碼中被調(diào)用的位置(不是被聲明的位置)。我們必須考察調(diào)用點(diǎn)來(lái)回答這個(gè)問(wèn)題:這個(gè) this
指向什么?
一般來(lái)說(shuō)尋找調(diào)用點(diǎn)就是:“找到一個(gè)函數(shù)是在哪里被調(diào)用的”,但它不總是那么簡(jiǎn)單,比如某些特定的編碼模式會(huì)使 真正的 調(diào)用點(diǎn)變得不那么明確。
考慮 調(diào)用棧(call-stack) (使我們到達(dá)當(dāng)前執(zhí)行位置而被調(diào)用的所有方法的堆棧)是十分重要的。我們關(guān)心的調(diào)用點(diǎn)就位于當(dāng)前執(zhí)行中的函數(shù) 之前 的調(diào)用。
我們來(lái)展示一下調(diào)用棧和調(diào)用點(diǎn):
function baz() {
// 調(diào)用棧是: `baz`
// 我們的調(diào)用點(diǎn)是 global scope(全局作用域)
console.log( "baz" );
bar(); // <-- `bar` 的調(diào)用點(diǎn)
}
function bar() {
// 調(diào)用棧是: `baz` -> `bar`
// 我們的調(diào)用點(diǎn)位于 `baz`
console.log( "bar" );
foo(); // <-- `foo` 的 call-site
}
function foo() {
// 調(diào)用棧是: `baz` -> `bar` -> `foo`
// 我們的調(diào)用點(diǎn)位于 `bar`
console.log( "foo" );
}
baz(); // <-- `baz` 的調(diào)用點(diǎn)
在分析代碼來(lái)尋找(從調(diào)用棧中)真正的調(diào)用點(diǎn)時(shí)要小心,因?yàn)樗怯绊?this
綁定的唯一因素。
注意: 你可以通過(guò)按順序觀(guān)察函數(shù)的調(diào)用鏈在你的大腦中建立調(diào)用棧的視圖,就像我們?cè)谏厦娲a段中的注釋那樣。但是這很痛苦而且易錯(cuò)。另一種觀(guān)察調(diào)用棧的方式是使用你的瀏覽器的調(diào)試工具。大多數(shù)現(xiàn)代的桌面瀏覽器都內(nèi)建開(kāi)發(fā)者工具,其中就包含 JS 調(diào)試器。在上面的代碼段中,你可以在調(diào)試工具中為 foo()
函數(shù)的第一行設(shè)置一個(gè)斷點(diǎn),或者簡(jiǎn)單的在這第一行上插入一個(gè) debugger
語(yǔ)句。當(dāng)你運(yùn)行這個(gè)網(wǎng)頁(yè)時(shí),調(diào)試工具將會(huì)停止在這個(gè)位置,并且向你展示一個(gè)到達(dá)這一行之前所有被調(diào)用過(guò)的函數(shù)的列表,這就是你的調(diào)用棧。所以,如果你想調(diào)查this
綁定,可以使用開(kāi)發(fā)者工具取得調(diào)用棧,之后從上向下找到第二個(gè)記錄,那就是你真正的調(diào)用點(diǎn)。
僅僅是規(guī)則
現(xiàn)在我們將注意力轉(zhuǎn)移到調(diào)用點(diǎn) 如何 決定在函數(shù)執(zhí)行期間 this
指向哪里。
你必須考察調(diào)用點(diǎn)并判定4種規(guī)則中的哪一種適用。我們將首先獨(dú)立地解釋一下這4種規(guī)則中的每一種,之后我們來(lái)展示一下如果有多種規(guī)則可以適用于調(diào)用點(diǎn)時(shí),它們的優(yōu)先順序。
默認(rèn)綁定(Default Binding)
我們要考察的第一種規(guī)則源于函數(shù)調(diào)用的最常見(jiàn)的情況:獨(dú)立函數(shù)調(diào)用。可以認(rèn)為這種 this
規(guī)則是在沒(méi)有其他規(guī)則適用時(shí)的默認(rèn)規(guī)則。
考慮這個(gè)代碼段:
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
第一點(diǎn)要注意的,如果你還沒(méi)有察覺(jué)到,是在全局作用域中的聲明變量,也就是var a = 2
,是全局對(duì)象的同名屬性的同義詞。它們不是互相拷貝對(duì)方,它們 就是 彼此。正如一個(gè)硬幣的兩面。
第二,我們看到當(dāng)foo()
被調(diào)用時(shí),this.a
解析為我們的全局變量a
。為什么?因?yàn)樵谶@種情況下,對(duì)此方法調(diào)用的 this
實(shí)施了 默認(rèn)綁定,所以使 this
指向了全局對(duì)象。
我們?cè)趺粗肋@里適用 默認(rèn)綁定 ?我們考察調(diào)用點(diǎn)來(lái)看看 foo()
是如何被調(diào)用的。在我們的代碼段中,foo()
是被一個(gè)直白的,毫無(wú)修飾的函數(shù)引用調(diào)用的。沒(méi)有其他的我們將要展示的規(guī)則適用于這里,所以 默認(rèn)綁定 在這里適用。
如果 strict mode
在這里生效,那么對(duì)于 默認(rèn)綁定 來(lái)說(shuō)全局對(duì)象是不合法的,所以 this
將被設(shè)置為 undefined
。
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: `this` is `undefined`
一個(gè)微妙但是重要的細(xì)節(jié)是:即便所有的 this
綁定規(guī)則都是完全基于調(diào)用點(diǎn)的,但如果 foo()
的 內(nèi)容 沒(méi)有在 strict mode
下執(zhí)行,對(duì)于 默認(rèn)綁定 來(lái)說(shuō)全局對(duì)象是 唯一 合法的;foo()
的調(diào)用點(diǎn)的 strict mode
狀態(tài)與此無(wú)關(guān)。
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();
注意: 在你的代碼中故意混用 strict mode
和非 strict mode
通常是讓人皺眉頭的。你的程序整體可能應(yīng)當(dāng)不是 Strict 就是 非 Strict。然而,有時(shí)你可能會(huì)引用與你的 Strict 模式不同的第三方包,所以對(duì)這些微妙的兼容性細(xì)節(jié)要多加小心。
隱含綁定(Implicit Binding)
另一種要考慮的規(guī)則是:調(diào)用點(diǎn)是否有一個(gè)環(huán)境對(duì)象(context object),也稱(chēng)為擁有者(owning)或容器(containing)對(duì)象,雖然這些名詞可能有些誤導(dǎo)人。
考慮這段代碼:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
首先,注意 foo()
被聲明然后作為引用屬性添加到 obj
上的方式。無(wú)論 foo()
是否一開(kāi)始就在 obj
上被聲明,還是后來(lái)作為引用添加(如上面代碼所示),這個(gè) 函數(shù) 都不被 obj
所真正“擁有”或“包含”。
然而,調(diào)用點(diǎn) 使用 obj
環(huán)境來(lái) 引用 函數(shù),所以你 可以說(shuō) obj
對(duì)象在函數(shù)被調(diào)用的時(shí)間點(diǎn)上“擁有”或“包含”這個(gè) 函數(shù)引用。
不論你怎樣稱(chēng)呼這個(gè)模式,在 foo()
被調(diào)用的位置上,它被冠以一個(gè)指向 obj
的對(duì)象引用。當(dāng)一個(gè)方法引用存在一個(gè)環(huán)境對(duì)象時(shí),隱含綁定 規(guī)則會(huì)說(shuō):是這個(gè)對(duì)象應(yīng)當(dāng)被用于這個(gè)函數(shù)調(diào)用的 this
綁定。
因?yàn)?obj
是 foo()
調(diào)用的 this
,所以 this.a
就是 obj.a
的同義詞。
只有對(duì)象屬性引用鏈的最后一層是影響調(diào)用點(diǎn)的。比如:
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
隱含丟失(Implicitly Lost)
this
綁定最常讓人沮喪的事情之一,就是當(dāng)一個(gè) 隱含綁定 丟失了它的綁定,這通常意味著它會(huì)退回到 默認(rèn)綁定, 根據(jù) strict mode
的狀態(tài),其結(jié)果不是全局對(duì)象就是 undefined
。
考慮這段代碼:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函數(shù)引用!
var a = "oops, global"; // `a` 也是一個(gè)全局對(duì)象的屬性
bar(); // "oops, global"
盡管 bar
似乎是 obj.foo
的引用,但實(shí)際上它只是另一個(gè) foo
本身的引用而已。另外,起作用的調(diào)用點(diǎn)是 bar()
,一個(gè)直白,毫無(wú)修飾的調(diào)用,因此 默認(rèn)綁定 適用于這里。
這種情況發(fā)生的更加微妙,更常見(jiàn),而且更意外的方式,是當(dāng)我們考慮傳遞一個(gè)回調(diào)函數(shù)時(shí):
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// `fn` 只不過(guò) `foo` 的另一個(gè)引用
fn(); // <-- 調(diào)用點(diǎn)!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // `a` 也是一個(gè)全局對(duì)象的屬性
doFoo( obj.foo ); // "oops, global"
參數(shù)傳遞僅僅是一種隱含的賦值,而且因?yàn)槲覀冊(cè)趥鬟f一個(gè)函數(shù),它是一個(gè)隱含的引用賦值,所以最終結(jié)果和我們前一個(gè)代碼段一樣。
那么如果接收你所傳遞回調(diào)的函數(shù)不是你的,而是語(yǔ)言?xún)?nèi)建的呢?沒(méi)有區(qū)別,同樣的結(jié)果。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // `a` 也是一個(gè)全局對(duì)象的屬性
setTimeout( obj.foo, 100 ); // "oops, global"
把這個(gè)粗糙的,理論上的 setTimeout()
假想實(shí)現(xiàn)當(dāng)做 JavaScript 環(huán)境內(nèi)建的實(shí)現(xiàn)的話(huà):
function setTimeout(fn,delay) {
// (通過(guò)某種方法)等待 `delay` 毫秒
fn(); // <-- 調(diào)用點(diǎn)!
}
正如我們剛剛看到的,我們的回調(diào)函數(shù)丟掉他們的 this
綁定是十分常見(jiàn)的事情。但是 this
使我們吃驚的另一種方式是,接收我們回調(diào)的函數(shù)故意改變調(diào)用的 this
。那些很流行的 JavaScript 庫(kù)中的事件處理器就十分喜歡強(qiáng)制你的回調(diào)的 this
指向觸發(fā)事件的 DOM 元素。雖然有時(shí)這很有用,但其他時(shí)候這簡(jiǎn)直能氣死人。不幸的是,這些工具很少給你選擇。
不管哪一種意外改變 this
的方式,你都不能真正地控制你的回調(diào)函數(shù)引用將如何被執(zhí)行,所以你(還)沒(méi)有辦法控制調(diào)用點(diǎn)給你一個(gè)故意的綁定。我們很快就會(huì)看到一個(gè)方法,通過(guò) 固定 this
來(lái)解決這個(gè)問(wèn)題。
明確綁定(Explicit Binding)
用我們剛看到的 隱含綁定,我們不得不改變目標(biāo)對(duì)象使它自身包含一個(gè)對(duì)函數(shù)的引用,而后使用這個(gè)函數(shù)引用屬性來(lái)間接地(隱含地)將 this
綁定到這個(gè)對(duì)象上。
但是,如果你想強(qiáng)制一個(gè)函數(shù)調(diào)用使用某個(gè)特定對(duì)象作為 this
綁定,而不在這個(gè)對(duì)象上放置一個(gè)函數(shù)引用屬性呢?
JavaScript 語(yǔ)言中的“所有”函數(shù)都有一些工具(通過(guò)他們的 [[Prototype]]
—— 待會(huì)兒詳述)可以用于這個(gè)任務(wù)。具體地說(shuō),函數(shù)擁有 call(..)
和 apply(..)
方法。從技術(shù)上講,JavaScript 宿主環(huán)境有時(shí)會(huì)提供一些(說(shuō)得好聽(tīng)點(diǎn)兒!)很特別的函數(shù),它們沒(méi)有這些功能。但這很少見(jiàn)。絕大多數(shù)被提供的函數(shù),當(dāng)然還有你將創(chuàng)建的所有的函數(shù),都可以訪(fǎng)問(wèn) call(..)
和 apply(..)
。
這些工具如何工作?它們接收的第一個(gè)參數(shù)都是一個(gè)用于 this
的對(duì)象,之后使用這個(gè)指定的 this
來(lái)調(diào)用函數(shù)。因?yàn)槟阋呀?jīng)直接指明你想讓 this
是什么,所以我們稱(chēng)這種方式為 明確綁定(explicit binding)。
考慮這段代碼:
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
通過(guò) foo.call(..)
使用 明確綁定 來(lái)調(diào)用 foo
,允許我們強(qiáng)制函數(shù)的 this
指向 obj
。
如果你傳遞一個(gè)簡(jiǎn)單基本類(lèi)型值(string
,boolean
,或 number
類(lèi)型)作為 this
綁定,那么這個(gè)基本類(lèi)型值會(huì)被包裝在它的對(duì)象類(lèi)型中(分別是 new String(..)
,new Boolean(..)
,或 new Number(..)
)。這通常稱(chēng)為“封箱(boxing)”。
注意: 就 this
綁定的角度講,call(..)
和 apply(..)
是完全一樣的。它們確實(shí)在處理其他參數(shù)上的方式不同,但那不是我們當(dāng)前關(guān)心的。
不幸的是,單獨(dú)依靠 明確綁定 仍然不能為我們先前提到的問(wèn)題提供解決方案,也就是函數(shù)“丟失”自己原本的 this
綁定,或者被第三方框架覆蓋,等等問(wèn)題。
硬綁定(Hard Binding)
但是有一個(gè) 明確綁定 的變種確實(shí)可以實(shí)現(xiàn)這個(gè)技巧。考慮這段代碼:
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// `bar` 將 `foo` 的 `this` 硬綁定到 `obj`
// 所以它不可以被覆蓋
bar.call( window ); // 2
我們來(lái)看看這個(gè)變種是如何工作的。我們創(chuàng)建了一個(gè)函數(shù) bar()
,在它的內(nèi)部手動(dòng)調(diào)用 foo.call(obj)
,由此強(qiáng)制 this
綁定到 obj
并調(diào)用 foo
。無(wú)論你過(guò)后怎樣調(diào)用函數(shù) bar
,它總是手動(dòng)使用 obj
調(diào)用 foo
。這種綁定即明確又堅(jiān)定,所以我們稱(chēng)之為 硬綁定(hard binding)
用 硬綁定 將一個(gè)函數(shù)包裝起來(lái)的最典型的方法,是為所有傳入的參數(shù)和傳出的返回值創(chuàng)建一個(gè)通道:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
};
var bar = function() {
return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5
另一種表達(dá)這種模式的方法是創(chuàng)建一個(gè)可復(fù)用的幫助函數(shù):
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
// 簡(jiǎn)單的 `bind` 幫助函數(shù)
function bind(fn, obj) {
return function() {
return fn.apply( obj, arguments );
};
}
var obj = {
a: 2
};
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
由于 硬綁定 是一個(gè)如此常用的模式,它已作為 ES5 的內(nèi)建工具提供:Function.prototype.bind
,像這樣使用:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
bind(..)
返回一個(gè)硬編碼的新函數(shù),它使用你指定的 this
環(huán)境來(lái)調(diào)用原本的函數(shù)。
注意: 在 ES6 中,bind(..)
生成的硬綁定函數(shù)有一個(gè)名為 .name
的屬性,它源自于原始的 目標(biāo)函數(shù)(target function)。舉例來(lái)說(shuō):bar = foo.bind(..)
應(yīng)該會(huì)有一個(gè) bar.name
屬性,它的值為 "bound foo"
,這個(gè)值應(yīng)當(dāng)會(huì)顯示在調(diào)用棧軌跡的函數(shù)調(diào)用名稱(chēng)中。
API 調(diào)用的“環(huán)境”
確實(shí),許多庫(kù)中的函數(shù),和許多在 JavaScript 語(yǔ)言以及宿主環(huán)境中的內(nèi)建函數(shù),都提供一個(gè)可選參數(shù),通常稱(chēng)為“環(huán)境(context)”,這種設(shè)計(jì)作為一種替代方案來(lái)確保你的回調(diào)函數(shù)使用特定的 this
而不必非得使用 bind(..)
。
舉例來(lái)說(shuō):
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 使用 `obj` 作為 `this` 來(lái)調(diào)用 `foo(..)`
[1, 2, 3].forEach( foo, obj ); // 1 awesome 2 awesome 3 awesome
從內(nèi)部來(lái)說(shuō),幾乎可以確定這種類(lèi)型的函數(shù)是通過(guò) call(..)
或 apply(..)
來(lái)使用 明確綁定 以節(jié)省你的麻煩。
new
綁定(new
Binding)
第四種也是最后一種 this
綁定規(guī)則,要求我們重新思考 JavaScript 中關(guān)于函數(shù)和對(duì)象的常見(jiàn)誤解。
在傳統(tǒng)的面向類(lèi)語(yǔ)言中,“構(gòu)造器”是附著在類(lèi)上的一種特殊方法,當(dāng)使用 new
操作符來(lái)初始化一個(gè)類(lèi)時(shí),這個(gè)類(lèi)的構(gòu)造器就會(huì)被調(diào)用。通常看起來(lái)像這樣:
something = new MyClass(..);
JavaScript 擁有 new
操作符,而且使用它的代碼模式看起來(lái)和我們?cè)诿嫦蝾?lèi)語(yǔ)言中看到的基本一樣;大多數(shù)開(kāi)發(fā)者猜測(cè) JavaScript 機(jī)制在做某種相似的事情。但是,實(shí)際上 JavaScript 的機(jī)制和 new
在 JS 中的用法所暗示的面向類(lèi)的功能 沒(méi)有任何聯(lián)系。
首先,讓我們重新定義 JavaScript 的“構(gòu)造器”是什么。在 JS 中,構(gòu)造器 僅僅是一個(gè)函數(shù),它們偶然地與前置的 new
操作符一起調(diào)用。它們不依附于類(lèi),它們也不初始化一個(gè)類(lèi)。它們甚至不是一種特殊的函數(shù)類(lèi)型。它們本質(zhì)上只是一般的函數(shù),在被使用 new
來(lái)調(diào)用時(shí)改變了行為。
例如,引用 ES5.1 的語(yǔ)言規(guī)范,Number(..)
函數(shù)作為一個(gè)構(gòu)造器來(lái)說(shuō):
15.7.2 Number 構(gòu)造器
當(dāng) Number 作為 new 表達(dá)式的一部分被調(diào)用時(shí),它是一個(gè)構(gòu)造器:它初始化這個(gè)新創(chuàng)建的對(duì)象。
所以,可以說(shuō)任何函數(shù),包括像 Number(..)
(見(jiàn)第三章)這樣的內(nèi)建對(duì)象函數(shù)都可以在前面加上 new
來(lái)被調(diào)用,這使函數(shù)調(diào)用成為一個(gè) 構(gòu)造器調(diào)用(constructor call)。這是一個(gè)重要而微妙的區(qū)別:實(shí)際上不存在“構(gòu)造器函數(shù)”這樣的東西,而只有函數(shù)的構(gòu)造器調(diào)用。
當(dāng)在函數(shù)前面被加入 new
調(diào)用時(shí),也就是構(gòu)造器調(diào)用時(shí),下面這些事情會(huì)自動(dòng)完成:
- 一個(gè)全新的對(duì)象會(huì)憑空創(chuàng)建(就是被構(gòu)建)
- 這個(gè)新構(gòu)建的對(duì)象會(huì)被接入原形鏈(
[[Prototype]]
-linked) - 這個(gè)新構(gòu)建的對(duì)象被設(shè)置為函數(shù)調(diào)用的
this
綁定 - 除非函數(shù)返回一個(gè)它自己的其他 對(duì)象,否則這個(gè)被
new
調(diào)用的函數(shù)將 自動(dòng) 返回這個(gè)新構(gòu)建的對(duì)象。
步驟 1,3 和 4 是我們當(dāng)下要討論的。我們現(xiàn)在跳過(guò)第 2 步,在第五章回過(guò)頭來(lái)討論。
考慮這段代碼:
function foo(a) {
this.a = a;
}
var bar = new foo( 2 );
console.log( bar.a ); // 2
通過(guò)在前面使用 new
來(lái)調(diào)用 foo(..)
,我們構(gòu)建了一個(gè)新的對(duì)象并把這個(gè)新對(duì)象作為 foo(..)
調(diào)用的 this
。 new
是函數(shù)調(diào)用可以綁定 this
的最后一種方式,我們稱(chēng)之為 new 綁定(new binding)。
一切皆有順序
如此,我們已經(jīng)揭示了函數(shù)調(diào)用中的四種 this
綁定規(guī)則。你需要做的 一切 就是找到調(diào)用點(diǎn)然后考察哪一種規(guī)則適用于它。但是,如果調(diào)用點(diǎn)上有多種規(guī)則都適用呢?這些規(guī)則一定有一個(gè)優(yōu)先順序,我們下面就來(lái)展示這些規(guī)則以什么樣的優(yōu)先順序?qū)嵤?/p>
很顯然,默認(rèn)綁定 在四種規(guī)則中優(yōu)先權(quán)最低的。所以我們先把它放在一邊。
隱含綁定 和 明確綁定 哪一個(gè)更優(yōu)先呢?我們來(lái)測(cè)試一下:
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
所以, 明確綁定 的優(yōu)先權(quán)要高于 隱含綁定,這意味著你應(yīng)當(dāng)在考察 隱含綁定 之前 首先 考察 明確綁定 是否適用。
現(xiàn)在,我們只需要搞清楚 new 綁定 的優(yōu)先級(jí)位于何處。
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
好了,new 綁定 的優(yōu)先級(jí)要高于 隱含綁定。那么你覺(jué)得 new 綁定 的優(yōu)先級(jí)較之于 明確綁定 是高還是低呢?
注意: new
和 call
/apply
不能同時(shí)使用,所以 new foo.call(obj1)
是不允許的,也就是不能直接對(duì)比測(cè)試 new 綁定 和 明確綁定。但是我們依然可以使用 硬綁定 來(lái)測(cè)試這兩個(gè)規(guī)則的優(yōu)先級(jí)。
在我們進(jìn)入代碼中探索之前,回想一下 硬綁定 物理上是如何工作的,也就是 Function.prototype.bind(..)
創(chuàng)建了一個(gè)新的包裝函數(shù),這個(gè)函數(shù)被硬編碼為忽略它自己的 this
綁定(不管它是什么),轉(zhuǎn)而手動(dòng)使用我們提供的。
因此,這似乎看起來(lái)很明顯,硬綁定(明確綁定的一種)的優(yōu)先級(jí)要比 new 綁定 高,而且不能被 new
覆蓋。
我們檢驗(yàn)一下:
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
哇!bar
是硬綁定到 obj1
的,但是 new bar(3)
并 沒(méi)有 像我們期待的那樣將 obj1.a
變?yōu)?3
。反而,硬綁定(到 obj1
)的 bar(..)
調(diào)用 可以 被 new
所覆蓋。因?yàn)?new
被實(shí)施,我們得到一個(gè)名為 baz
的新創(chuàng)建的對(duì)象,而且我們確實(shí)看到 baz.a
的值為 3
。
如果你回頭看看我們的“山寨”綁定幫助函數(shù),這很令人吃驚:
function bind(fn, obj) {
return function() {
fn.apply( obj, arguments );
};
}
如果你推導(dǎo)這段幫助代碼如何工作,會(huì)發(fā)現(xiàn)對(duì)于 new
操作符調(diào)用來(lái)說(shuō)沒(méi)有辦法去像我們觀(guān)察到的那樣,將綁定到 obj
的硬綁定覆蓋。
但是 ES5 的內(nèi)建 Function.prototype.bind(..)
更加精妙,實(shí)際上十分精妙。這里是 MDN 網(wǎng)頁(yè)上為 bind(..)
提供的(稍稍格式化后的)polyfill(低版本兼容填補(bǔ)工具):
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== "function") {
// 可能的與 ECMAScript 5 內(nèi)部的 IsCallable 函數(shù)最接近的東西,
throw new TypeError( "Function.prototype.bind - what " +
"is trying to be bound is not callable"
);
}
var aArgs = Array.prototype.slice.call( arguments, 1 ),
fToBind = this,
fNOP = function(){},
fBound = function(){
return fToBind.apply(
(
this instanceof fNOP &&
oThis ? this : oThis
),
aArgs.concat( Array.prototype.slice.call( arguments ) )
);
}
;
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
注意: 就將與 new
一起使用的硬綁定函數(shù)(參照下面來(lái)看為什么這有用)而言,上面的 bind(..)
polyfill 與 ES5 中內(nèi)建的 bind(..)
是不同的。因?yàn)?polyfill 不能像內(nèi)建工具那樣,沒(méi)有 .prototype
就能創(chuàng)建函數(shù),這里使用了一些微妙而間接的方法來(lái)近似模擬相同的行為。如果你打算將硬綁定函數(shù)和 new
一起使用而且依賴(lài)于這個(gè) polyfill,應(yīng)當(dāng)多加小心。
允許 new
進(jìn)行覆蓋的部分是這里:
this instanceof fNOP &&
oThis ? this : oThis
// ... 和:
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
我們不會(huì)實(shí)際深入解釋這個(gè)花招兒是如何工作的(這很復(fù)雜而且超出了我們當(dāng)前的討論范圍),但實(shí)質(zhì)上這個(gè)工具判斷硬綁定函數(shù)是否是通過(guò) new
被調(diào)用的(導(dǎo)致一個(gè)新構(gòu)建的對(duì)象作為它的 this
),如果是,它就用那個(gè)新構(gòu)建的 this
而非先前為 this
指定的 硬綁定。
為什么 new
可以覆蓋 硬綁定 這件事很有用?
這種行為的主要原因是,創(chuàng)建一個(gè)實(shí)質(zhì)上忽略 this
的 硬綁定 而預(yù)先設(shè)置一部分或所有的參數(shù)的函數(shù)(這個(gè)函數(shù)可以與 new
一起使用來(lái)構(gòu)建對(duì)象)。bind(..)
的一個(gè)能力是,任何在第一個(gè) this
綁定參數(shù)之后被傳入的參數(shù),默認(rèn)地作為當(dāng)前函數(shù)的標(biāo)準(zhǔn)參數(shù)(技術(shù)上這稱(chēng)為“局部應(yīng)用(partial application)”,是一種“柯里化(currying)”)。
例如:
function foo(p1,p2) {
this.val = p1 + p2;
}
// 在這里使用 `null` 是因?yàn)樵谶@種場(chǎng)景下我們不關(guān)心 `this` 的硬綁定
// 而且反正它將會(huì)被 `new` 調(diào)用覆蓋掉!
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2
判定 this
現(xiàn)在,我們可以按照優(yōu)先順序來(lái)總結(jié)一下從函數(shù)調(diào)用的調(diào)用點(diǎn)來(lái)判定 this
的規(guī)則了。按照這個(gè)順序來(lái)問(wèn)問(wèn)題,然后在第一個(gè)規(guī)則適用的地方停下。
-
函數(shù)是通過(guò)
new
被調(diào)用的嗎(new 綁定)?如果是,this
就是新構(gòu)建的對(duì)象。var bar = new foo()
-
函數(shù)是通過(guò)
call
或apply
被調(diào)用(明確綁定),甚至是隱藏在bind
硬綁定 之中嗎?如果是,this
就是那個(gè)被明確指定的對(duì)象。var bar = foo.call( obj2 )
-
函數(shù)是通過(guò)環(huán)境對(duì)象(也稱(chēng)為擁有者或容器對(duì)象)被調(diào)用的嗎(隱含綁定)?如果是,
this
就是那個(gè)環(huán)境對(duì)象。var bar = obj1.foo()
-
否則,使用默認(rèn)的
this
(默認(rèn)綁定)。如果在strict mode
下,就是undefined
,否則是global
對(duì)象。var bar = foo()
以上,就是理解對(duì)于普通的函數(shù)調(diào)用來(lái)說(shuō)的 this
綁定規(guī)則 所需的全部。是的……幾乎是全部。
綁定的特例
正如通常的那樣,對(duì)于“規(guī)則”總有一些 例外。
在某些場(chǎng)景下 this
綁定會(huì)讓人很吃驚,比如在你試圖實(shí)施一種綁定,然而最終得到的卻是 默認(rèn)綁定 規(guī)則的綁定行為(見(jiàn)前面的內(nèi)容)。
被忽略的 this
如果你傳遞 null
或 undefined
作為 call
、apply
或 bind
的 this
綁定參數(shù),那么這些值會(huì)被忽略掉,取而代之的是 默認(rèn)綁定 規(guī)則將適用于這個(gè)調(diào)用。
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
為什么你會(huì)向 this
綁定故意傳遞像 null
這樣的值?
一個(gè)很常見(jiàn)的做法是,使用 apply(..)
來(lái)將一個(gè)數(shù)組散開(kāi),從而作為函數(shù)調(diào)用的參數(shù)。相似地,bind(..)
可以柯里化參數(shù)(預(yù)設(shè)值),也可能非常有用。
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 將數(shù)組散開(kāi)作為參數(shù)
foo.apply( null, [2, 3] ); // a:2, b:3
// 用 `bind(..)` 進(jìn)行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
這兩種工具都要求第一個(gè)參數(shù)是 this
綁定。如果目標(biāo)函數(shù)不關(guān)心 this
,你就需要一個(gè)占位值,而且正如這個(gè)代碼段中展示的,null
看起來(lái)是一個(gè)合理的選擇。
注意: 雖然我們?cè)谶@本書(shū)中沒(méi)有涵蓋,但是 ES6 中有一個(gè)擴(kuò)散操作符:...
,它讓你無(wú)需使用 apply(..)
而在語(yǔ)法上將一個(gè)數(shù)組“散開(kāi)”作為參數(shù),比如 foo(...[1,2])
表示 foo(1,2)
—— 如果 this
綁定沒(méi)有必要,可以在語(yǔ)法上回避它。不幸的是,柯里化在 ES6 中沒(méi)有語(yǔ)法上的替代品,所以 bind(..)
調(diào)用的 this
參數(shù)依然需要注意。
可是,在你不關(guān)心 this
綁定而一直使用 null
的時(shí)候,有些潛在的“危險(xiǎn)”。如果你這樣處理一些函數(shù)調(diào)用(比如,不歸你管控的第三方包),而且那些函數(shù)確實(shí)使用了 this
引用,那么 默認(rèn)綁定 規(guī)則意味著它可能會(huì)不經(jīng)意間引用(或者改變,更糟糕!)global
對(duì)象(在瀏覽器中是 window
)。
很顯然,這樣的陷阱會(huì)導(dǎo)致多種 非常難 診斷和追蹤的 Bug。
更安全的 this
也許某些“更安全”的做法是:為了 this
而傳遞一個(gè)特殊創(chuàng)建好的對(duì)象,這個(gè)對(duì)象保證不會(huì)對(duì)你的程序產(chǎn)生副作用。從網(wǎng)絡(luò)學(xué)(或軍事)上借用一個(gè)詞,我們可以建立一個(gè)“DMZ”(非軍事區(qū))對(duì)象 —— 只不過(guò)是一個(gè)完全為空,沒(méi)有委托(見(jiàn)第五,六章)的對(duì)象。
如果我們?yōu)榱撕雎宰约赫J(rèn)為不用關(guān)心的 this
綁定,而總是傳遞一個(gè) DMZ 對(duì)象,那么我們就可以確定任何對(duì) this
的隱藏或意外的使用將會(huì)被限制在這個(gè)空對(duì)象中,也就是說(shuō)這個(gè)對(duì)象將 global
對(duì)象和副作用隔離開(kāi)來(lái)。
因?yàn)檫@個(gè)對(duì)象是完全為空的,我個(gè)人喜歡給它一個(gè)變量名為 ?
(空集合的數(shù)學(xué)符號(hào)的小寫(xiě))。在許多鍵盤(pán)上(比如 Mac 的美式鍵盤(pán)),這個(gè)符號(hào)可以很容易地用 ?
+o
(option+o
)打出來(lái)。有些系統(tǒng)還允許你為某個(gè)特殊符號(hào)設(shè)置快捷鍵。如果你不喜歡 ?
符號(hào),或者你的鍵盤(pán)沒(méi)那么好打,你當(dāng)然可以叫它任意你希望的名字。
無(wú)論你叫它什么,創(chuàng)建 完全為空的對(duì)象 的最簡(jiǎn)單方法就是 Object.create(null)
(見(jiàn)第五章)。Object.create(null)
和 {}
很相似,但是沒(méi)有指向 Object.prototype
的委托,所以它比 {}
“空得更徹底”。
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我們的 DMZ 空對(duì)象
var ? = Object.create( null );
// 將數(shù)組散開(kāi)作為參數(shù)
foo.apply( ?, [2, 3] ); // a:2, b:3
// 用 `bind(..)` 進(jìn)行 currying
var bar = foo.bind( ?, 2 );
bar( 3 ); // a:2, b:3
不僅在功能上更“安全”,?
還會(huì)在代碼風(fēng)格上產(chǎn)生些好處,它在語(yǔ)義上可能會(huì)比 null
更清晰的表達(dá)“我想讓 this
為空”。當(dāng)然,你可以隨自己喜歡來(lái)稱(chēng)呼你的 DMZ 對(duì)象。
間接
另外一個(gè)要注意的是,你可以(有意或無(wú)意地!)創(chuàng)建對(duì)函數(shù)的“間接引用(indirect reference)”,在那樣的情況下,當(dāng)那個(gè)函數(shù)引用被調(diào)用時(shí),默認(rèn)綁定 規(guī)則也會(huì)適用。
一個(gè)最常見(jiàn)的 間接引用 產(chǎn)生方式是通過(guò)賦值:
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
賦值表達(dá)式 p.foo = o.foo
的 結(jié)果值 是一個(gè)剛好指向底層函數(shù)對(duì)象的引用。如此,起作用的調(diào)用點(diǎn)就是 foo()
,而非你期待的 p.foo()
或 o.foo()
。根據(jù)上面的規(guī)則,默認(rèn)綁定 適用。
提醒: 無(wú)論你如何得到適用 默認(rèn)綁定 的函數(shù)調(diào)用,被調(diào)用函數(shù)的 內(nèi)容 的 strict mode
狀態(tài) —— 而非函數(shù)的調(diào)用點(diǎn) —— 決定了 this
引用的值:不是 global
對(duì)象(在非 strict mode
下),就是 undefined
(在 strict mode
下)。
軟化綁定(Softening Binding)
我們之前看到 硬綁定 是一種通過(guò)將函數(shù)強(qiáng)制綁定到特定的 this
上,來(lái)防止函數(shù)調(diào)用在不經(jīng)意間退回到 默認(rèn)綁定 的策略(除非你用 new
去覆蓋它!)。問(wèn)題是,硬綁定 極大地降低了函數(shù)的靈活性,阻止我們手動(dòng)使用 隱含綁定 或后續(xù)的 明確綁定 來(lái)覆蓋 this
。
如果有這樣的辦法就好了:為 默認(rèn)綁定 提供不同的默認(rèn)值(不是 global
或 undefined
),同時(shí)保持函數(shù)可以通過(guò) 隱含綁定 或 明確綁定 技術(shù)來(lái)手動(dòng)綁定 this
。
我們可以構(gòu)建一個(gè)所謂的 軟綁定 工具來(lái)模擬我們期望的行為。
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this,
curried = [].slice.call( arguments, 1 ),
bound = function bound() {
return fn.apply(
(!this ||
(typeof window !== "undefined" &&
this === window) ||
(typeof global !== "undefined" &&
this === global)
) ? obj : this,
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
這里提供的 softBind(..)
工具的工作方式和 ES5 內(nèi)建的 bind(..)
工具很相似,除了我們的 軟綁定 行為。它用一種邏輯將指定的函數(shù)包裝起來(lái),這個(gè)邏輯在函數(shù)調(diào)用時(shí)檢查 this
,如果它是 global
或 undefined
,就使用預(yù)先指定的 默認(rèn)值 (obj
),否則保持 this
不變。它也提供了可選的柯里化行為(見(jiàn)先前的 bind(..)
討論)。
我們來(lái)看看它的用法:
function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 ); // name: obj <---- 退回到軟綁定
軟綁定版本的 foo()
函數(shù)可以如展示的那樣被手動(dòng) this
綁定到 obj2
或 obj3
,如果 默認(rèn)綁定 適用時(shí)會(huì)退到 obj
。
詞法 this
我們剛剛涵蓋了一般函數(shù)遵守的四種規(guī)則。但是 ES6 引入了一種不適用于這些規(guī)則特殊的函數(shù):箭頭函數(shù)(arrow-function)。
箭頭函數(shù)不是通過(guò) function
關(guān)鍵字聲明的,而是通過(guò)所謂的“大箭頭”操作符:=>
。與使用四種標(biāo)準(zhǔn)的 this
規(guī)則不同的是,箭頭函數(shù)從封閉它的(函數(shù)或全局)作用域采用 this
綁定。
我們來(lái)展示一下箭頭函數(shù)的詞法作用域:
function foo() {
// 返回一個(gè)箭頭函數(shù)
return (a) => {
// 這里的 `this` 是詞法上從 `foo()` 采用的
console.log( this.a );
};
}
var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是3!
在 foo()
中創(chuàng)建的箭頭函數(shù)在詞法上捕獲 foo()
被調(diào)用時(shí)的 this
,不管它是什么。因?yàn)?foo()
被 this
綁定到 obj1
,bar
(被返回的箭頭函數(shù)的一個(gè)引用)也將會(huì)被 this
綁定到 obj1
。一個(gè)箭頭函數(shù)的詞法綁定是不能被覆蓋的(就連 new
也不行!)。
最常見(jiàn)的用法是用于回調(diào),比如事件處理器或計(jì)時(shí)器:
function foo() {
setTimeout(() => {
// 這里的 `this` 是詞法上從 `foo()` 采用
console.log( this.a );
},100);
}
var obj = {
a: 2
};
foo.call( obj ); // 2
雖然箭頭函數(shù)提供除了使用 bind(..)
外,另外一種在函數(shù)上來(lái)確保 this
的方式,這看起來(lái)很吸引人,但重要的是要注意它們本質(zhì)是使用廣為人知的詞法作用域來(lái)禁止了傳統(tǒng)的 this
機(jī)制。在 ES6 之前,為此我們已經(jīng)有了相當(dāng)常用的模式,這些模式幾乎和 ES6 的箭頭函數(shù)的精神沒(méi)有區(qū)別:
function foo() {
var self = this; // 詞法上捕獲 `this`
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
雖然對(duì)不想用 bind(..)
的人來(lái)說(shuō) self = this
和箭頭函數(shù)都是看起來(lái)不錯(cuò)的“解決方案”,但它們實(shí)質(zhì)上逃避了 this
而非理解和接受它。
如果你發(fā)現(xiàn)你在寫(xiě) this
風(fēng)格的代碼,但是大多數(shù)或全部時(shí)候,你都用詞法上的 self = this
或箭頭函數(shù)“技巧”抵御 this
機(jī)制,那么也許你應(yīng)該:
僅使用詞法作用域并忘掉虛偽的
this
風(fēng)格代碼。完全接受
this
風(fēng)格機(jī)制,包括在必要的時(shí)候使用bind(..)
,并嘗試避開(kāi)self = this
和箭頭函數(shù)的“詞法 this”技巧。
一個(gè)程序可以有效地同時(shí)利用兩種風(fēng)格的代碼(詞法和 this
),但是在同一個(gè)函數(shù)內(nèi)部,特別是對(duì)同種類(lèi)型的查找,混合這兩種機(jī)制通常是自找很難維護(hù)的代碼,而且可能是聰明過(guò)了頭。
復(fù)習(xí)
為執(zhí)行中的函數(shù)判定 this
綁定需要找到這個(gè)函數(shù)的直接調(diào)用點(diǎn)。找到之后,四種規(guī)則將會(huì)以這種優(yōu)先順序施用于調(diào)用點(diǎn):
通過(guò)
new
調(diào)用?使用新構(gòu)建的對(duì)象。通過(guò)
call
或apply
(或bind
)調(diào)用?使用指定的對(duì)象。通過(guò)持有調(diào)用的環(huán)境對(duì)象調(diào)用?使用那個(gè)環(huán)境對(duì)象。
默認(rèn):
strict mode
下是undefined
,否則就是全局對(duì)象。
小心偶然或不經(jīng)意的 默認(rèn)綁定 規(guī)則調(diào)用。如果你想“安全”地忽略 this
綁定,一個(gè)像 ? = Object.create(null)
這樣的“DMZ”對(duì)象是一個(gè)很好的占位值,以保護(hù) global
對(duì)象不受意外的副作用影響。
與這四種綁定規(guī)則不同,ES6 的箭頭方法使用詞法作用域來(lái)決定 this
綁定,這意味著它們采用封閉他們的函數(shù)調(diào)用作為 this
綁定(無(wú)論它是什么)。它們實(shí)質(zhì)上是 ES6 之前的 self = this
代碼的語(yǔ)法替代品。