第二章: `this` 豁然開(kāi)朗!

特別說(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)?objfoo() 調(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)型值(stringboolean,或 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)完成:

  1. 一個(gè)全新的對(duì)象會(huì)憑空創(chuàng)建(就是被構(gòu)建)
  2. 這個(gè)新構(gòu)建的對(duì)象會(huì)被接入原形鏈([[Prototype]]-linked)
  3. 這個(gè)新構(gòu)建的對(duì)象被設(shè)置為函數(shù)調(diào)用的 this 綁定
  4. 除非函數(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)用的 thisnew 是函數(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í)較之于 明確綁定 是高還是低呢?

注意: newcall/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ī)則適用的地方停下。

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

    var bar = new foo()

  2. 函數(shù)是通過(guò) callapply 被調(diào)用(明確綁定),甚至是隱藏在 bind 硬綁定 之中嗎?如果是,this 就是那個(gè)被明確指定的對(duì)象。

    var bar = foo.call( obj2 )

  3. 函數(shù)是通過(guò)環(huán)境對(duì)象(也稱(chēng)為擁有者或容器對(duì)象)被調(diào)用的嗎(隱含綁定)?如果是,this 就是那個(gè)環(huán)境對(duì)象。

    var bar = obj1.foo()

  4. 否則,使用默認(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

如果你傳遞 nullundefined 作為 callapplybindthis 綁定參數(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)值(不是 globalundefined),同時(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,如果它是 globalundefined,就使用預(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 綁定到 obj2obj3,如果 默認(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 綁定到 obj1bar(被返回的箭頭函數(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)該:

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

  2. 完全接受 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):

  1. 通過(guò) new 調(diào)用?使用新構(gòu)建的對(duì)象。

  2. 通過(guò) callapply(或 bind)調(diào)用?使用指定的對(duì)象。

  3. 通過(guò)持有調(diào)用的環(huán)境對(duì)象調(diào)用?使用那個(gè)環(huán)境對(duì)象。

  4. 默認(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ǔ)法替代品。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,694評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,672評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,965評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,690評(píng)論 6 413
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 56,019評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 43,188評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,718評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,438評(píng)論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,667評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,845評(píng)論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,252評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,590評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,384評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,635評(píng)論 2 380

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