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