第二章: `this` 豁然開朗!

特別說明浮毯,為便于查閱,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS

在第一章中泰鸡,我們摒棄了種種對 this 的誤解债蓝,并且知道了 this 是一個完全根據(jù)調(diào)用點(函數(shù)是如何被調(diào)用的)而為每次函數(shù)調(diào)用建立的綁定。

調(diào)用點(Call-site)

為了理解 this 綁定盛龄,我們不得不理解調(diào)用點:函數(shù)在代碼中被調(diào)用的位置(不是被聲明的位置)饰迹。我們必須考察調(diào)用點來回答這個問題:這個 this 指向什么?

一般來說尋找調(diào)用點就是:“找到一個函數(shù)是在哪里被調(diào)用的”余舶,但它不總是那么簡單啊鸭,比如某些特定的編碼模式會使 真正的 調(diào)用點變得不那么明確。

考慮 調(diào)用棧(call-stack) (使我們到達當(dāng)前執(zhí)行位置而被調(diào)用的所有方法的堆棧)是十分重要的匿值。我們關(guān)心的調(diào)用點就位于當(dāng)前執(zhí)行中的函數(shù) 之前 的調(diào)用赠制。

我們來展示一下調(diào)用棧和調(diào)用點:

function baz() {
    // 調(diào)用棧是: `baz`
    // 我們的調(diào)用點是 global scope(全局作用域)

    console.log( "baz" );
    bar(); // <-- `bar` 的調(diào)用點
}

function bar() {
    // 調(diào)用棧是: `baz` -> `bar`
    // 我們的調(diào)用點位于 `baz`

    console.log( "bar" );
    foo(); // <-- `foo` 的 call-site
}

function foo() {
    // 調(diào)用棧是: `baz` -> `bar` -> `foo`
    // 我們的調(diào)用點位于 `bar`

    console.log( "foo" );
}

baz(); // <-- `baz` 的調(diào)用點

在分析代碼來尋找(從調(diào)用棧中)真正的調(diào)用點時要小心,因為它是影響 this 綁定的唯一因素千扔。

注意: 你可以通過按順序觀察函數(shù)的調(diào)用鏈在你的大腦中建立調(diào)用棧的視圖憎妙,就像我們在上面代碼段中的注釋那樣库正。但是這很痛苦而且易錯曲楚。另一種觀察調(diào)用棧的方式是使用你的瀏覽器的調(diào)試工具。大多數(shù)現(xiàn)代的桌面瀏覽器都內(nèi)建開發(fā)者工具褥符,其中就包含 JS 調(diào)試器龙誊。在上面的代碼段中,你可以在調(diào)試工具中為 foo() 函數(shù)的第一行設(shè)置一個斷點喷楣,或者簡單的在這第一行上插入一個 debugger 語句趟大。當(dāng)你運行這個網(wǎng)頁時鹤树,調(diào)試工具將會停止在這個位置,并且向你展示一個到達這一行之前所有被調(diào)用過的函數(shù)的列表逊朽,這就是你的調(diào)用棧罕伯。所以,如果你想調(diào)查this 綁定叽讳,可以使用開發(fā)者工具取得調(diào)用棧追他,之后從上向下找到第二個記錄,那就是你真正的調(diào)用點岛蚤。

僅僅是規(guī)則

現(xiàn)在我們將注意力轉(zhuǎn)移到調(diào)用點 如何 決定在函數(shù)執(zhí)行期間 this 指向哪里邑狸。

你必須考察調(diào)用點并判定4種規(guī)則中的哪一種適用。我們將首先獨立地解釋一下這4種規(guī)則中的每一種涤妒,之后我們來展示一下如果有多種規(guī)則可以適用于調(diào)用點時单雾,它們的優(yōu)先順序。

默認綁定(Default Binding)

我們要考察的第一種規(guī)則源于函數(shù)調(diào)用的最常見的情況:獨立函數(shù)調(diào)用她紫」瓒眩可以認為這種 this 規(guī)則是在沒有其他規(guī)則適用時的默認規(guī)則。

考慮這個代碼段:

function foo() {
    console.log( this.a );
}

var a = 2;

foo(); // 2

第一點要注意的犁苏,如果你還沒有察覺到硬萍,是在全局作用域中的聲明變量,也就是var a = 2围详,是全局對象的同名屬性的同義詞朴乖。它們不是互相拷貝對方,它們 就是 彼此助赞。正如一個硬幣的兩面买羞。

第二,我們看到當(dāng)foo()被調(diào)用時雹食,this.a解析為我們的全局變量a畜普。為什么?因為在這種情況下群叶,對此方法調(diào)用的 this 實施了 默認綁定吃挑,所以使 this 指向了全局對象。

我們怎么知道這里適用 默認綁定 街立?我們考察調(diào)用點來看看 foo() 是如何被調(diào)用的舶衬。在我們的代碼段中,foo() 是被一個直白的赎离,毫無修飾的函數(shù)引用調(diào)用的逛犹。沒有其他的我們將要展示的規(guī)則適用于這里,所以 默認綁定 在這里適用。

如果 strict mode 在這里生效虽画,那么對于 默認綁定 來說全局對象是不合法的舞蔽,所以 this 將被設(shè)置為 undefined

function foo() {
    "use strict";

    console.log( this.a );
}

var a = 2;

foo(); // TypeError: `this` is `undefined`

一個微妙但是重要的細節(jié)是:即便所有的 this 綁定規(guī)則都是完全基于調(diào)用點的码撰,但如果 foo()內(nèi)容 沒有在 strict mode 下執(zhí)行渗柿,對于 默認綁定 來說全局對象是 唯一 合法的;foo() 的調(diào)用點的 strict mode 狀態(tài)與此無關(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。然而鸡岗,有時你可能會引用與你的 Strict 模式不同的第三方包混槐,所以對這些微妙的兼容性細節(jié)要多加小心。

隱含綁定(Implicit Binding)

另一種要考慮的規(guī)則是:調(diào)用點是否有一個環(huán)境對象(context object)轩性,也稱為擁有者(owning)或容器(containing)對象声登,雖然這些名詞可能有些誤導(dǎo)人。

考慮這段代碼:

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2

首先揣苏,注意 foo() 被聲明然后作為引用屬性添加到 obj 上的方式悯嗓。無論 foo() 是否一開始就在 obj 上被聲明,還是后來作為引用添加(如上面代碼所示)卸察,這個 函數(shù) 都不被 obj 所真正“擁有”或“包含”脯厨。

然而,調(diào)用點 使用 obj 環(huán)境來 引用 函數(shù)坑质,所以你 可以說 obj 對象在函數(shù)被調(diào)用的時間點上“擁有”或“包含”這個 函數(shù)引用合武。

不論你怎樣稱呼這個模式,在 foo() 被調(diào)用的位置上涡扼,它被冠以一個指向 obj 的對象引用稼跳。當(dāng)一個方法引用存在一個環(huán)境對象時,隱含綁定 規(guī)則會說:是這個對象應(yīng)當(dāng)被用于這個函數(shù)調(diào)用的 this 綁定吃沪。

因為 objfoo() 調(diào)用的 this汤善,所以 this.a 就是 obj.a 的同義詞。

只有對象屬性引用鏈的最后一層是影響調(diào)用點的票彪。比如:

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)一個 隱含綁定 丟失了它的綁定,這通常意味著它會退回到 默認綁定降铸, 根據(jù) strict mode 的狀態(tài)在旱,其結(jié)果不是全局對象就是 undefined

考慮這段代碼:

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2,
    foo: foo
};

var bar = obj.foo; // 函數(shù)引用垮耳!

var a = "oops, global"; // `a` 也是一個全局對象的屬性

bar(); // "oops, global"

盡管 bar 似乎是 obj.foo 的引用颈渊,但實際上它只是另一個 foo 本身的引用而已。另外终佛,起作用的調(diào)用點是 bar()俊嗽,一個直白,毫無修飾的調(diào)用铃彰,因此 默認綁定 適用于這里绍豁。

這種情況發(fā)生的更加微妙,更常見牙捉,而且更意外的方式竹揍,是當(dāng)我們考慮傳遞一個回調(diào)函數(shù)時:

function foo() {
    console.log( this.a );
}

function doFoo(fn) {
    // `fn` 只不過 `foo` 的另一個引用

    fn(); // <-- 調(diào)用點!
}

var obj = {
    a: 2,
    foo: foo
};

var a = "oops, global"; // `a` 也是一個全局對象的屬性

doFoo( obj.foo ); // "oops, global"

參數(shù)傳遞僅僅是一種隱含的賦值,而且因為我們在傳遞一個函數(shù)邪铲,它是一個隱含的引用賦值芬位,所以最終結(jié)果和我們前一個代碼段一樣。

那么如果接收你所傳遞回調(diào)的函數(shù)不是你的带到,而是語言內(nèi)建的呢昧碉?沒有區(qū)別,同樣的結(jié)果揽惹。

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2,
    foo: foo
};

var a = "oops, global"; // `a` 也是一個全局對象的屬性

setTimeout( obj.foo, 100 ); // "oops, global"

把這個粗糙的被饿,理論上的 setTimeout() 假想實現(xiàn)當(dāng)做 JavaScript 環(huán)境內(nèi)建的實現(xiàn)的話:

function setTimeout(fn,delay) {
  // (通過某種方法)等待 `delay` 毫秒
    fn(); // <-- 調(diào)用點!
}

正如我們剛剛看到的,我們的回調(diào)函數(shù)丟掉他們的 this 綁定是十分常見的事情搪搏。但是 this 使我們吃驚的另一種方式是狭握,接收我們回調(diào)的函數(shù)故意改變調(diào)用的 this。那些很流行的 JavaScript 庫中的事件處理器就十分喜歡強制你的回調(diào)的 this 指向觸發(fā)事件的 DOM 元素疯溺。雖然有時這很有用论颅,但其他時候這簡直能氣死人。不幸的是囱嫩,這些工具很少給你選擇嗅辣。

不管哪一種意外改變 this 的方式,你都不能真正地控制你的回調(diào)函數(shù)引用將如何被執(zhí)行挠说,所以你(還)沒有辦法控制調(diào)用點給你一個故意的綁定澡谭。我們很快就會看到一個方法,通過 固定 this 來解決這個問題损俭。

明確綁定(Explicit Binding)

用我們剛看到的 隱含綁定蛙奖,我們不得不改變目標(biāo)對象使它自身包含一個對函數(shù)的引用,而后使用這個函數(shù)引用屬性來間接地(隱含地)將 this 綁定到這個對象上杆兵。

但是雁仲,如果你想強制一個函數(shù)調(diào)用使用某個特定對象作為 this 綁定,而不在這個對象上放置一個函數(shù)引用屬性呢琐脏?

JavaScript 語言中的“所有”函數(shù)都有一些工具(通過他們的 [[Prototype]] —— 待會兒詳述)可以用于這個任務(wù)攒砖。具體地說缸兔,函數(shù)擁有 call(..)apply(..) 方法。從技術(shù)上講吹艇,JavaScript 宿主環(huán)境有時會提供一些(說得好聽點兒6杳邸)很特別的函數(shù),它們沒有這些功能受神。但這很少見抛猖。絕大多數(shù)被提供的函數(shù),當(dāng)然還有你將創(chuàng)建的所有的函數(shù)鼻听,都可以訪問 call(..)apply(..)财著。

這些工具如何工作?它們接收的第一個參數(shù)都是一個用于 this 的對象撑碴,之后使用這個指定的 this 來調(diào)用函數(shù)撑教。因為你已經(jīng)直接指明你想讓 this 是什么,所以我們稱這種方式為 明確綁定(explicit binding)醉拓。

考慮這段代碼:

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2
};

foo.call( obj ); // 2

通過 foo.call(..) 使用 明確綁定 來調(diào)用 foo驮履,允許我們強制函數(shù)的 this 指向 obj

如果你傳遞一個簡單基本類型值(string廉嚼,boolean玫镐,或 number 類型)作為 this 綁定,那么這個基本類型值會被包裝在它的對象類型中(分別是 new String(..)怠噪,new Boolean(..)恐似,或 new Number(..))。這通常稱為“封箱(boxing)”傍念。

注意:this 綁定的角度講矫夷,call(..)apply(..) 是完全一樣的。它們確實在處理其他參數(shù)上的方式不同憋槐,但那不是我們當(dāng)前關(guān)心的双藕。

不幸的是,單獨依靠 明確綁定 仍然不能為我們先前提到的問題提供解決方案阳仔,也就是函數(shù)“丟失”自己原本的 this 綁定忧陪,或者被第三方框架覆蓋,等等問題近范。

硬綁定(Hard Binding)

但是有一個 明確綁定 的變種確實可以實現(xiàn)這個技巧嘶摊。考慮這段代碼:

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

我們來看看這個變種是如何工作的评矩。我們創(chuàng)建了一個函數(shù) bar()叶堆,在它的內(nèi)部手動調(diào)用 foo.call(obj),由此強制 this 綁定到 obj 并調(diào)用 foo斥杜。無論你過后怎樣調(diào)用函數(shù) bar虱颗,它總是手動使用 obj 調(diào)用 foo沥匈。這種綁定即明確又堅定,所以我們稱之為 硬綁定(hard binding)

硬綁定 將一個函數(shù)包裝起來的最典型的方法忘渔,是為所有傳入的參數(shù)和傳出的返回值創(chuàng)建一個通道:

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

另一種表達這種模式的方法是創(chuàng)建一個可復(fù)用的幫助函數(shù):

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}

// 簡單的 `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

由于 硬綁定 是一個如此常用的模式高帖,它已作為 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(..) 返回一個硬編碼的新函數(shù)辨萍,它使用你指定的 this 環(huán)境來調(diào)用原本的函數(shù)。

注意: 在 ES6 中返弹,bind(..) 生成的硬綁定函數(shù)有一個名為 .name 的屬性锈玉,它源自于原始的 目標(biāo)函數(shù)(target function)。舉例來說:bar = foo.bind(..) 應(yīng)該會有一個 bar.name 屬性义起,它的值為 "bound foo"拉背,這個值應(yīng)當(dāng)會顯示在調(diào)用棧軌跡的函數(shù)調(diào)用名稱中。

API 調(diào)用的“環(huán)境”

確實默终,許多庫中的函數(shù)椅棺,和許多在 JavaScript 語言以及宿主環(huán)境中的內(nèi)建函數(shù),都提供一個可選參數(shù)齐蔽,通常稱為“環(huán)境(context)”两疚,這種設(shè)計作為一種替代方案來確保你的回調(diào)函數(shù)使用特定的 this 而不必非得使用 bind(..)

舉例來說:

function foo(el) {
    console.log( el, this.id );
}

var obj = {
    id: "awesome"
};

// 使用 `obj` 作為 `this` 來調(diào)用 `foo(..)`
[1, 2, 3].forEach( foo, obj ); // 1 awesome  2 awesome  3 awesome

從內(nèi)部來說含滴,幾乎可以確定這種類型的函數(shù)是通過 call(..)apply(..) 來使用 明確綁定 以節(jié)省你的麻煩诱渤。

new 綁定(new Binding)

第四種也是最后一種 this 綁定規(guī)則,要求我們重新思考 JavaScript 中關(guān)于函數(shù)和對象的常見誤解谈况。

在傳統(tǒng)的面向類語言中勺美,“構(gòu)造器”是附著在類上的一種特殊方法,當(dāng)使用 new 操作符來初始化一個類時碑韵,這個類的構(gòu)造器就會被調(diào)用赡茸。通橙猿樱看起來像這樣:

something = new MyClass(..);

JavaScript 擁有 new 操作符症见,而且使用它的代碼模式看起來和我們在面向類語言中看到的基本一樣声滥;大多數(shù)開發(fā)者猜測 JavaScript 機制在做某種相似的事情吧趣。但是笛厦,實際上 JavaScript 的機制和 new 在 JS 中的用法所暗示的面向類的功能 沒有任何聯(lián)系赁还。

首先旨枯,讓我們重新定義 JavaScript 的“構(gòu)造器”是什么趟佃。在 JS 中耸袜,構(gòu)造器 僅僅是一個函數(shù)友多,它們偶然地與前置的 new 操作符一起調(diào)用。它們不依附于類堤框,它們也不初始化一個類域滥。它們甚至不是一種特殊的函數(shù)類型纵柿。它們本質(zhì)上只是一般的函數(shù),在被使用 new 來調(diào)用時改變了行為启绰。

例如昂儒,引用 ES5.1 的語言規(guī)范,Number(..) 函數(shù)作為一個構(gòu)造器來說:

15.7.2 Number 構(gòu)造器

當(dāng) Number 作為 new 表達式的一部分被調(diào)用時委可,它是一個構(gòu)造器:它初始化這個新創(chuàng)建的對象渊跋。

所以,可以說任何函數(shù)着倾,包括像 Number(..)(見第三章)這樣的內(nèi)建對象函數(shù)都可以在前面加上 new 來被調(diào)用拾酝,這使函數(shù)調(diào)用成為一個 構(gòu)造器調(diào)用(constructor call)。這是一個重要而微妙的區(qū)別:實際上不存在“構(gòu)造器函數(shù)”這樣的東西卡者,而只有函數(shù)的構(gòu)造器調(diào)用蒿囤。

當(dāng)在函數(shù)前面被加入 new 調(diào)用時,也就是構(gòu)造器調(diào)用時崇决,下面這些事情會自動完成:

  1. 一個全新的對象會憑空創(chuàng)建(就是被構(gòu)建)
  2. 這個新構(gòu)建的對象會被接入原形鏈([[Prototype]]-linked)
  3. 這個新構(gòu)建的對象被設(shè)置為函數(shù)調(diào)用的 this 綁定
  4. 除非函數(shù)返回一個它自己的其他 對象材诽,否則這個被 new 調(diào)用的函數(shù)將 自動 返回這個新構(gòu)建的對象。

步驟 1恒傻,3 和 4 是我們當(dāng)下要討論的脸侥。我們現(xiàn)在跳過第 2 步,在第五章回過頭來討論盈厘。

考慮這段代碼:

function foo(a) {
    this.a = a;
}

var bar = new foo( 2 );
console.log( bar.a ); // 2

通過在前面使用 new 來調(diào)用 foo(..)湿痢,我們構(gòu)建了一個新的對象并把這個新對象作為 foo(..) 調(diào)用的 thisnew 是函數(shù)調(diào)用可以綁定 this 的最后一種方式扑庞,我們稱之為 new 綁定(new binding)譬重。

一切皆有順序

如此,我們已經(jīng)揭示了函數(shù)調(diào)用中的四種 this 綁定規(guī)則罐氨。你需要做的 一切 就是找到調(diào)用點然后考察哪一種規(guī)則適用于它臀规。但是,如果調(diào)用點上有多種規(guī)則都適用呢栅隐?這些規(guī)則一定有一個優(yōu)先順序塔嬉,我們下面就來展示這些規(guī)則以什么樣的優(yōu)先順序?qū)嵤?/p>

很顯然,默認綁定 在四種規(guī)則中優(yōu)先權(quán)最低的租悄。所以我們先把它放在一邊谨究。

隱含綁定明確綁定 哪一個更優(yōu)先呢?我們來測試一下:

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)先級位于何處潭辈。

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)先級要高于 隱含綁定澈吨。那么你覺得 new 綁定 的優(yōu)先級較之于 明確綁定 是高還是低呢?

注意: newcall/apply 不能同時使用寄摆,所以 new foo.call(obj1) 是不允許的谅辣,也就是不能直接對比測試 new 綁定明確綁定。但是我們依然可以使用 硬綁定 來測試這兩個規(guī)則的優(yōu)先級婶恼。

在我們進入代碼中探索之前桑阶,回想一下 硬綁定 物理上是如何工作的,也就是 Function.prototype.bind(..) 創(chuàng)建了一個新的包裝函數(shù)勾邦,這個函數(shù)被硬編碼為忽略它自己的 this 綁定(不管它是什么)蚣录,轉(zhuǎn)而手動使用我們提供的。

因此检痰,這似乎看起來很明顯包归,硬綁定明確綁定的一種)的優(yōu)先級要比 new 綁定 高锨推,而且不能被 new 覆蓋铅歼。

我們檢驗一下:

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 變?yōu)?3椎椰。反而,硬綁定(到 obj1)的 bar(..) 調(diào)用 可以new 所覆蓋沾鳄。因為 new 被實施慨飘,我們得到一個名為 baz 的新創(chuàng)建的對象,而且我們確實看到 baz.a 的值為 3译荞。

如果你回頭看看我們的“山寨”綁定幫助函數(shù)瓤的,這很令人吃驚:

function bind(fn, obj) {
    return function() {
        fn.apply( obj, arguments );
    };
}

如果你推導(dǎo)這段幫助代碼如何工作,會發(fā)現(xiàn)對于 new 操作符調(diào)用來說沒有辦法去像我們觀察到的那樣吞歼,將綁定到 obj 的硬綁定覆蓋圈膏。

但是 ES5 的內(nèi)建 Function.prototype.bind(..) 更加精妙,實際上十分精妙篙骡。這里是 MDN 網(wǎng)頁上為 bind(..) 提供的(稍稍格式化后的)polyfill(低版本兼容填補工具):

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ù)(參照下面來看為什么這有用)而言,上面的 bind(..) polyfill 與 ES5 中內(nèi)建的 bind(..) 是不同的糯俗。因為 polyfill 不能像內(nèi)建工具那樣尿褪,沒有 .prototype 就能創(chuàng)建函數(shù),這里使用了一些微妙而間接的方法來近似模擬相同的行為得湘。如果你打算將硬綁定函數(shù)和 new 一起使用而且依賴于這個 polyfill杖玲,應(yīng)當(dāng)多加小心。

允許 new 進行覆蓋的部分是這里:

this instanceof fNOP &&
oThis ? this : oThis

// ... 和:

fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();

我們不會實際深入解釋這個花招兒是如何工作的(這很復(fù)雜而且超出了我們當(dāng)前的討論范圍)淘正,但實質(zhì)上這個工具判斷硬綁定函數(shù)是否是通過 new 被調(diào)用的(導(dǎo)致一個新構(gòu)建的對象作為它的 this)天揖,如果是夺欲,它就用那個新構(gòu)建的 this 而非先前為 this 指定的 硬綁定

為什么 new 可以覆蓋 硬綁定 這件事很有用今膊?

這種行為的主要原因是些阅,創(chuàng)建一個實質(zhì)上忽略 this硬綁定 而預(yù)先設(shè)置一部分或所有的參數(shù)的函數(shù)(這個函數(shù)可以與 new 一起使用來構(gòu)建對象)。bind(..) 的一個能力是斑唬,任何在第一個 this 綁定參數(shù)之后被傳入的參數(shù)市埋,默認地作為當(dāng)前函數(shù)的標(biāo)準(zhǔn)參數(shù)(技術(shù)上這稱為“局部應(yīng)用(partial application)”,是一種“柯里化(currying)”)恕刘。

例如:

function foo(p1,p2) {
    this.val = p1 + p2;
}

// 在這里使用 `null` 是因為在這種場景下我們不關(guān)心 `this` 的硬綁定
// 而且反正它將會被 `new` 調(diào)用覆蓋掉缤谎!
var bar = foo.bind( null, "p1" );

var baz = new bar( "p2" );

baz.val; // p1p2

判定 this

現(xiàn)在,我們可以按照優(yōu)先順序來總結(jié)一下從函數(shù)調(diào)用的調(diào)用點來判定 this 的規(guī)則了褐着。按照這個順序來問問題坷澡,然后在第一個規(guī)則適用的地方停下。

  1. 函數(shù)是通過 new 被調(diào)用的嗎(new 綁定)含蓉?如果是频敛,this 就是新構(gòu)建的對象。

    var bar = new foo()

  2. 函數(shù)是通過 callapply 被調(diào)用(明確綁定)馅扣,甚至是隱藏在 bind 硬綁定 之中嗎斟赚?如果是,this 就是那個被明確指定的對象差油。

    var bar = foo.call( obj2 )

  3. 函數(shù)是通過環(huán)境對象(也稱為擁有者或容器對象)被調(diào)用的嗎(隱含綁定)拗军?如果是,this 就是那個環(huán)境對象蓄喇。

    var bar = obj1.foo()

  4. 否則发侵,使用默認的 this默認綁定)。如果在 strict mode 下妆偏,就是 undefined刃鳄,否則是 global 對象。

    var bar = foo()

以上楼眷,就是理解對于普通的函數(shù)調(diào)用來說的 this 綁定規(guī)則 所需的全部铲汪。是的……幾乎是全部。

綁定的特例

正如通常的那樣罐柳,對于“規(guī)則”總有一些 例外掌腰。

在某些場景下 this 綁定會讓人很吃驚,比如在你試圖實施一種綁定张吉,然而最終得到的卻是 默認綁定 規(guī)則的綁定行為(見前面的內(nèi)容)齿梁。

被忽略的 this

如果你傳遞 nullundefined 作為 callapplybindthis 綁定參數(shù),那么這些值會被忽略掉勺择,取而代之的是 默認綁定 規(guī)則將適用于這個調(diào)用创南。

function foo() {
    console.log( this.a );
}

var a = 2;

foo.call( null ); // 2

為什么你會向 this 綁定故意傳遞像 null 這樣的值?

一個很常見的做法是省核,使用 apply(..) 來將一個數(shù)組散開稿辙,從而作為函數(shù)調(diào)用的參數(shù)。相似地气忠,bind(..) 可以柯里化參數(shù)(預(yù)設(shè)值)邻储,也可能非常有用。

function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}

// 將數(shù)組散開作為參數(shù)
foo.apply( null, [2, 3] ); // a:2, b:3

// 用 `bind(..)` 進行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3

這兩種工具都要求第一個參數(shù)是 this 綁定旧噪。如果目標(biāo)函數(shù)不關(guān)心 this吨娜,你就需要一個占位值,而且正如這個代碼段中展示的淘钟,null 看起來是一個合理的選擇宦赠。

注意: 雖然我們在這本書中沒有涵蓋,但是 ES6 中有一個擴散操作符:...米母,它讓你無需使用 apply(..) 而在語法上將一個數(shù)組“散開”作為參數(shù)勾扭,比如 foo(...[1,2]) 表示 foo(1,2) —— 如果 this 綁定沒有必要,可以在語法上回避它爱咬。不幸的是尺借,柯里化在 ES6 中沒有語法上的替代品绊起,所以 bind(..) 調(diào)用的 this 參數(shù)依然需要注意精拟。

可是,在你不關(guān)心 this 綁定而一直使用 null 的時候虱歪,有些潛在的“危險”蜂绎。如果你這樣處理一些函數(shù)調(diào)用(比如,不歸你管控的第三方包)笋鄙,而且那些函數(shù)確實使用了 this 引用师枣,那么 默認綁定 規(guī)則意味著它可能會不經(jīng)意間引用(或者改變,更糟糕O袈洹)global 對象(在瀏覽器中是 window)践美。

很顯然,這樣的陷阱會導(dǎo)致多種 非常難 診斷和追蹤的 Bug找岖。

更安全的 this

也許某些“更安全”的做法是:為了 this 而傳遞一個特殊創(chuàng)建好的對象陨倡,這個對象保證不會對你的程序產(chǎn)生副作用。從網(wǎng)絡(luò)學(xué)(或軍事)上借用一個詞许布,我們可以建立一個“DMZ”(非軍事區(qū))對象 —— 只不過是一個完全為空兴革,沒有委托(見第五,六章)的對象。

如果我們?yōu)榱撕雎宰约赫J為不用關(guān)心的 this 綁定杂曲,而總是傳遞一個 DMZ 對象庶艾,那么我們就可以確定任何對 this 的隱藏或意外的使用將會被限制在這個空對象中,也就是說這個對象將 global 對象和副作用隔離開來擎勘。

因為這個對象是完全為空的咱揍,我個人喜歡給它一個變量名為 ?(空集合的數(shù)學(xué)符號的小寫)。在許多鍵盤上(比如 Mac 的美式鍵盤)棚饵,這個符號可以很容易地用 ?+o(option+o)打出來述召。有些系統(tǒng)還允許你為某個特殊符號設(shè)置快捷鍵。如果你不喜歡 ? 符號蟹地,或者你的鍵盤沒那么好打积暖,你當(dāng)然可以叫它任意你希望的名字。

無論你叫它什么怪与,創(chuàng)建 完全為空的對象 的最簡單方法就是 Object.create(null)(見第五章)夺刑。Object.create(null){} 很相似,但是沒有指向 Object.prototype 的委托分别,所以它比 {} “空得更徹底”遍愿。

function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}

// 我們的 DMZ 空對象
var ? = Object.create( null );

// 將數(shù)組散開作為參數(shù)
foo.apply( ?, [2, 3] ); // a:2, b:3

// 用 `bind(..)` 進行 currying
var bar = foo.bind( ?, 2 );
bar( 3 ); // a:2, b:3

不僅在功能上更“安全”,? 還會在代碼風(fēng)格上產(chǎn)生些好處耘斩,它在語義上可能會比 null 更清晰的表達“我想讓 this 為空”沼填。當(dāng)然,你可以隨自己喜歡來稱呼你的 DMZ 對象括授。

間接

另外一個要注意的是坞笙,你可以(有意或無意地!)創(chuàng)建對函數(shù)的“間接引用(indirect reference)”荚虚,在那樣的情況下薛夜,當(dāng)那個函數(shù)引用被調(diào)用時,默認綁定 規(guī)則也會適用版述。

一個最常見的 間接引用 產(chǎn)生方式是通過賦值:

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結(jié)果值 是一個剛好指向底層函數(shù)對象的引用梯澜。如此,起作用的調(diào)用點就是 foo()渴析,而非你期待的 p.foo()o.foo()晚伙。根據(jù)上面的規(guī)則,默認綁定 適用俭茧。

提醒: 無論你如何得到適用 默認綁定 的函數(shù)調(diào)用咆疗,被調(diào)用函數(shù)的 內(nèi)容strict mode 狀態(tài) —— 而非函數(shù)的調(diào)用點 —— 決定了 this 引用的值:不是 global 對象(在非 strict mode 下),就是 undefined(在 strict mode 下)恢恼。

軟化綁定(Softening Binding)

我們之前看到 硬綁定 是一種通過將函數(shù)強制綁定到特定的 this 上民傻,來防止函數(shù)調(diào)用在不經(jīng)意間退回到 默認綁定 的策略(除非你用 new 去覆蓋它!)。問題是漓踢,硬綁定 極大地降低了函數(shù)的靈活性牵署,阻止我們手動使用 隱含綁定 或后續(xù)的 明確綁定 來覆蓋 this

如果有這樣的辦法就好了:為 默認綁定 提供不同的默認值(不是 globalundefined)喧半,同時保持函數(shù)可以通過 隱含綁定明確綁定 技術(shù)來手動綁定 this奴迅。

我們可以構(gòu)建一個所謂的 軟綁定 工具來模擬我們期望的行為。

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ù)包裝起來,這個邏輯在函數(shù)調(diào)用時檢查 this扁耐,如果它是 globalundefined暇检,就使用預(yù)先指定的 默認值obj),否則保持 this 不變婉称。它也提供了可選的柯里化行為(見先前的 bind(..) 討論)块仆。

我們來看看它的用法:

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ù)可以如展示的那樣被手動 this 綁定到 obj2obj3,如果 默認綁定 適用時會退到 obj王暗。

詞法 this

我們剛剛涵蓋了一般函數(shù)遵守的四種規(guī)則悔据。但是 ES6 引入了一種不適用于這些規(guī)則特殊的函數(shù):箭頭函數(shù)(arrow-function)。

箭頭函數(shù)不是通過 function 關(guān)鍵字聲明的俗壹,而是通過所謂的“大箭頭”操作符:=>科汗。與使用四種標(biāo)準(zhǔn)的 this 規(guī)則不同的是,箭頭函數(shù)從封閉它的(函數(shù)或全局)作用域采用 this 綁定绷雏。

我們來展示一下箭頭函數(shù)的詞法作用域:

function foo() {
  // 返回一個箭頭函數(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)用時的 this头滔,不管它是什么。因為 foo()this 綁定到 obj1之众,bar(被返回的箭頭函數(shù)的一個引用)也將會被 this 綁定到 obj1拙毫。一個箭頭函數(shù)的詞法綁定是不能被覆蓋的(就連 new 也不行R佬怼)棺禾。

最常見的用法是用于回調(diào),比如事件處理器或計時器:

function foo() {
    setTimeout(() => {
        // 這里的 `this` 是詞法上從 `foo()` 采用
        console.log( this.a );
    },100);
}

var obj = {
    a: 2
};

foo.call( obj ); // 2

雖然箭頭函數(shù)提供除了使用 bind(..) 外峭跳,另外一種在函數(shù)上來確保 this 的方式膘婶,這看起來很吸引人,但重要的是要注意它們本質(zhì)是使用廣為人知的詞法作用域來禁止了傳統(tǒng)的 this 機制蛀醉。在 ES6 之前悬襟,為此我們已經(jīng)有了相當(dāng)常用的模式,這些模式幾乎和 ES6 的箭頭函數(shù)的精神沒有區(qū)別:

function foo() {
    var self = this; // 詞法上捕獲 `this`
    setTimeout( function(){
        console.log( self.a );
    }, 100 );
}

var obj = {
    a: 2
};

foo.call( obj ); // 2

雖然對不想用 bind(..) 的人來說 self = this 和箭頭函數(shù)都是看起來不錯的“解決方案”拯刁,但它們實質(zhì)上逃避了 this 而非理解和接受它脊岳。

如果你發(fā)現(xiàn)你在寫 this 風(fēng)格的代碼,但是大多數(shù)或全部時候,你都用詞法上的 self = this 或箭頭函數(shù)“技巧”抵御 this 機制割捅,那么也許你應(yīng)該:

  1. 僅使用詞法作用域并忘掉虛偽的 this 風(fēng)格代碼奶躯。

  2. 完全接受 this 風(fēng)格機制,包括在必要的時候使用 bind(..)亿驾,并嘗試避開 self = this 和箭頭函數(shù)的“詞法 this”技巧嘹黔。

一個程序可以有效地同時利用兩種風(fēng)格的代碼(詞法和 this),但是在同一個函數(shù)內(nèi)部莫瞬,特別是對同種類型的查找儡蔓,混合這兩種機制通常是自找很難維護的代碼,而且可能是聰明過了頭疼邀。

復(fù)習(xí)

為執(zhí)行中的函數(shù)判定 this 綁定需要找到這個函數(shù)的直接調(diào)用點喂江。找到之后,四種規(guī)則將會以這種優(yōu)先順序施用于調(diào)用點:

  1. 通過 new 調(diào)用旁振?使用新構(gòu)建的對象开呐。

  2. 通過 callapply(或 bind)調(diào)用?使用指定的對象规求。

  3. 通過持有調(diào)用的環(huán)境對象調(diào)用筐付?使用那個環(huán)境對象。

  4. 默認:strict mode 下是 undefined阻肿,否則就是全局對象瓦戚。

小心偶然或不經(jīng)意的 默認綁定 規(guī)則調(diào)用。如果你想“安全”地忽略 this 綁定丛塌,一個像 ? = Object.create(null) 這樣的“DMZ”對象是一個很好的占位值较解,以保護 global 對象不受意外的副作用影響。

與這四種綁定規(guī)則不同赴邻,ES6 的箭頭方法使用詞法作用域來決定 this 綁定印衔,這意味著它們采用封閉他們的函數(shù)調(diào)用作為 this 綁定(無論它是什么)。它們實質(zhì)上是 ES6 之前的 self = this 代碼的語法替代品姥敛。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末奸焙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子彤敛,更是在濱河造成了極大的恐慌与帆,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件墨榄,死亡現(xiàn)場離奇詭異玄糟,居然都是意外死亡,警方通過查閱死者的電腦和手機袄秩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門阵翎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逢并,“玉大人,你說我怎么就攤上這事郭卫⊥埠荩” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵箱沦,是天一觀的道長辩恼。 經(jīng)常有香客問我,道長谓形,這世上最難降的妖魔是什么灶伊? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮寒跳,結(jié)果婚禮上聘萨,老公的妹妹穿的比我還像新娘。我一直安慰自己童太,他們只是感情好米辐,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著书释,像睡著了一般翘贮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上爆惧,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天狸页,我揣著相機與錄音,去河邊找鬼扯再。 笑死芍耘,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的熄阻。 我是一名探鬼主播斋竞,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼秃殉!你這毒婦竟也來了坝初?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤复濒,失蹤者是張志新(化名)和其女友劉穎脖卖,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體巧颈,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年袖扛,在試婚紗的時候發(fā)現(xiàn)自己被綠了砸泛。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片十籍。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖唇礁,靈堂內(nèi)的尸體忽然破棺而出勾栗,到底是詐尸還是另有隱情,我是刑警寧澤盏筐,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布围俘,位于F島的核電站,受9級特大地震影響琢融,放射性物質(zhì)發(fā)生泄漏界牡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一漾抬、第九天 我趴在偏房一處隱蔽的房頂上張望宿亡。 院中可真熱鬧,春花似錦纳令、人聲如沸挽荠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽圈匆。三九已至,卻和暖如春捏雌,著一層夾襖步出監(jiān)牢的瞬間臭脓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工腹忽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留来累,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓窘奏,卻偏偏與公主長得像嘹锁,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子着裹,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容