你不懂JS:ES6與未來 第七章:元編程

官方中文版原文鏈接

感謝社區(qū)中各位的大力支持壶谒,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券黔姜,享受所有官網(wǎng)優(yōu)惠甘有,并抽取幸運(yùn)大獎:點(diǎn)擊這里領(lǐng)取

元編程是針對程序本身的行為進(jìn)行操作的編程律适。換句話說,它是為你程序的編程而進(jìn)行的編程袭祟。是的验残,很拗口,對吧巾乳?

例如胚膊,如果你為了調(diào)查對象a和另一個對象b之間的關(guān)系 —— 它們是被[[Prototype]]鏈接的嗎故俐? —— 而使用a.isPrototypeOf(b),這通常稱為自省紊婉,就是一種形式的元編程。宏(JS中還沒有) —— 代碼在編譯時修改自己 —— 是元編程的另一個明顯的例子辑舷。使用for..in循環(huán)枚舉一個對象的鍵喻犁,或者檢查一個對象是否是一個“類構(gòu)造器”的 實例,是另一些常見的元編程任務(wù)何缓。

元編程關(guān)注以下的一點(diǎn)或幾點(diǎn):代碼檢視自己肢础,代碼修改自己,或者代碼修改默認(rèn)的語言行為而使其他代碼受影響碌廓。

元編程的目標(biāo)是利用語言自身的內(nèi)在能力使你其他部分的代碼更具描述性传轰,表現(xiàn)力,和/或靈活性谷婆。由于元編程的 的性質(zhì)慨蛙,要給它一個更精確的定義有些困難。理解元編程的最佳方法是通過代碼來觀察它纪挎。

ES6在JS已經(jīng)擁有的東西上期贫,增加了幾種新的元編程形式/特性。

函數(shù)名

有一些情況异袄,你的代碼想要檢視自己并詢問某個函數(shù)的名稱是什么通砍。如果你詢問一個函數(shù)的名稱,答案會有些令人詫異地模糊烤蜕》馑铮考慮如下代碼:

function daz() {
    // ..
}

var obj = {
    foo: function() {
        // ..
    },
    bar: function baz() {
        // ..
    },
    bam: daz,
    zim() {
        // ..
    }
};

在這前一個代碼段中,“obj.foo()的名字是什么讽营?”有些微妙虎忌。是"foo"""斑匪,還是undefined呐籽?那么obj.bar()呢 —— 是"bar"還是"baz"obj.bam()稱為"bam"還是"daz"蚀瘸?obj.zim()呢狡蝶?

另外,作為回調(diào)被傳遞的函數(shù)呢贮勃?就像:

function foo(cb) {
    // 這里的 `cb()` 的名字是什么贪惹?
}

foo( function(){
    // 我是匿名的!
} );

在程序中函數(shù)可以被好幾種方法所表達(dá)寂嘉,而函數(shù)的“名字”應(yīng)當(dāng)是什么并不總是那么清晰和明確奏瞬。

更重要的是枫绅,我們需要區(qū)別函數(shù)的“名字”是指它的name屬性 —— 是的,函數(shù)有一個叫做name的屬性 —— 還是指它詞法綁定的名稱硼端,比如在function bar() { .. }中的bar并淋。

詞法綁定名稱是你將在遞歸之類的東西中所使用的:

function foo(i) {
    if (i < 10) return foo( i * 2 );
    return i;
}

name屬性是你為了元編程而使用的,所以它才是我們在這里的討論中所關(guān)注的珍昨。

產(chǎn)生這種用困惑是因為县耽,在默認(rèn)情況下一個函數(shù)的詞法名稱(如果有的話)也會被設(shè)置為它的name屬性。實際上镣典,ES5(和以前的)語言規(guī)范中并沒有官方要求這種行為兔毙。name屬性的設(shè)置是一種非標(biāo)準(zhǔn),但依然相當(dāng)可靠的行為兄春。在ES6中澎剥,它已經(jīng)被標(biāo)準(zhǔn)化。

提示: 如果一個函數(shù)的name被賦值赶舆,它通常是在開發(fā)者工具的棧軌跡中使用的名稱哑姚。

推斷

但如果函數(shù)沒有詞法名稱,name屬性會怎么樣呢涌乳?

現(xiàn)在在ES6中蜻懦,有一個推斷規(guī)則可以判定一個合理的name屬性值來賦予一個函數(shù),即使它沒有詞法名稱可用夕晓。

考慮如下代碼:

var abc = function() {
    // ..
};

abc.name;               // "abc"

如果我們給了這個函數(shù)一個詞法名稱宛乃,比如abc = function def() { .. },那么name屬性將理所當(dāng)然地是"def"蒸辆。但是由于缺少詞法名稱征炼,直觀上名稱"abc"看起來很合適。

這里是在ES6中將會(或不會)進(jìn)行名稱推斷的其他形式:

(function(){ .. });                 // name:
(function*(){ .. });                // name:
window.foo = function(){ .. };      // name:

class Awesome {
    constructor() { .. }            // name: Awesome
    funny() { .. }                  // name: funny
}

var c = class Awesome { .. };       // name: Awesome

var o = {
    foo() { .. },                   // name: foo
    *bar() { .. },                  // name: bar
    baz: () => { .. },              // name: baz
    bam: function(){ .. },          // name: bam
    get qux() { .. },               // name: get qux
    set fuz() { .. },               // name: set fuz
    ["b" + "iz"]:
        function(){ .. },           // name: biz
    [Symbol( "buz" )]:
        function(){ .. }            // name: [buz]
};

var x = o.foo.bind( o );            // name: bound foo
(function(){ .. }).bind( o );       // name: bound

export default function() { .. }    // name: default

var y = new Function();             // name: anonymous
var GeneratorFunction =
    function*(){}.__proto__.constructor;
var z = new GeneratorFunction();    // name: anonymous

name屬性默認(rèn)是不可寫的躬贡,但它是可配置的谆奥,這意味著如果有需要,你可以使用Object.defineProperty(..)來手動改變它拂玻。

元屬性

在第三章的“new.target”一節(jié)中酸些,我們引入了一個ES6的新概念:元屬性。正如這個名稱所暗示的檐蚜,元屬性意在以一種屬性訪問的形式提供特殊的元信息魄懂,而這在以前是不可能的。

new.target的情況下闯第,關(guān)鍵字new作為一個屬性訪問的上下文環(huán)境市栗。顯然new本身不是一個對象,這使得這種能力很特殊。然而填帽,當(dāng)new.target被用于一個構(gòu)造器調(diào)用(一個使用new調(diào)用的函數(shù)/方法)內(nèi)部時蛛淋,new變成了一個虛擬上下文環(huán)境,如此new.target就可以指代這個new調(diào)用的目標(biāo)構(gòu)造器篡腌。

這是一個元編程操作的典型例子褐荷,因為它的意圖是從一個構(gòu)造器調(diào)用內(nèi)部判定原來的new的目標(biāo)是什么,這一般是為了自拾ⅰ(檢查類型/結(jié)構(gòu))或者靜態(tài)屬性訪問诚卸。

舉例來說,你可能想根據(jù)一個構(gòu)造器是被直接調(diào)用绘迁,還是通過一個子類進(jìn)行調(diào)用,來使它有不同的行為:

class Parent {
    constructor() {
        if (new.target === Parent) {
            console.log( "Parent instantiated" );
        }
        else {
            console.log( "A child instantiated" );
        }
    }
}

class Child extends Parent {}

var a = new Parent();
// Parent instantiated

var b = new Child();
// A child instantiated

這里有一個微妙的地方卒密,在Parent類定義內(nèi)部的constructor()實際上被給予了這個類的詞法名稱(Parent)缀台,即便語法暗示著這個類是一個與構(gòu)造器分離的不同實體。

警告: 與所有的元編程技術(shù)一樣哮奇,要小心不要創(chuàng)建太過聰明的代碼膛腐,而使未來的你或其他維護(hù)你代碼的人很難理解。小心使用這些技巧鼎俘。

通用 Symbol

在第二章中的“Symbol”一節(jié)中哲身,我們講解了新的ES6基本類型symbol。除了你可以在你自己的程序中定義的symbol以外贸伐,JS預(yù)定義了幾種內(nèi)建symbol勘天,被稱為 通用(Well Known) Symbols(WKS)。

定義這些symbol值主要是為了向你的JS程序暴露特殊的元屬性來給你更多JS行為的控制權(quán)捉邢。

我們將簡要介紹每一個symbol并討論它們的目的脯丝。

Symbol.iterator

在第二和第三章中,我們介紹并使用了@@iteratorsymbol伏伐,它被自動地用于...擴(kuò)散和for..of循環(huán)宠进。我們還在第五章中看到了在新的ES6集合中定義的@@iterator

Symbol.iterator表示在任意一個對象上的特殊位置(屬性)藐翎,語言機(jī)制自動地在這里尋找一個方法材蹬,這個方法將構(gòu)建一個用于消費(fèi)對象值的迭代器對象。許多對象都帶有一個默認(rèn)的Symbol.iterator吝镣。

然而堤器,我們可以通過設(shè)置Symbol.iterator屬性來為任意對象定義我們自己的迭代器邏輯,即便它是覆蓋默認(rèn)迭代器的赤惊。這里的元編程觀點(diǎn)是吼旧,我們在定義JS的其他部分(明確地說,是操作符和循環(huán)結(jié)構(gòu))在處理我們所定義的對象值時所使用的行為未舟。

考慮如下代碼:

var arr = [4,5,6,7,8,9];

for (var v of arr) {
    console.log( v );
}
// 4 5 6 7 8 9

// 定義一個僅在奇數(shù)索引處產(chǎn)生值的迭代器
arr[Symbol.iterator] = function*() {
    var idx = 1;
    do {
        yield this[idx];
    } while ((idx += 2) < this.length);
};

for (var v of arr) {
    console.log( v );
}
// 5 7 9

Symbol.toStringTagSymbol.hasInstance

最常見的元編程任務(wù)之一圈暗,就是在一個值上進(jìn)行自省來找出它是什么 種類 的掂为,者經(jīng)常用來決定它們上面適于實施什么操作。對于對象员串,最常見的兩個自省技術(shù)是toString()instanceof勇哗。

考慮如下代碼:

function Foo() {}

var a = new Foo();

a.toString();               // [object Object]
a instanceof Foo;           // true

在ES6中,你可以控制這些操作的行為:

function Foo(greeting) {
    this.greeting = greeting;
}

Foo.prototype[Symbol.toStringTag] = "Foo";

Object.defineProperty( Foo, Symbol.hasInstance, {
    value: function(inst) {
        return inst.greeting == "hello";
    }
} );

var a = new Foo( "hello" ),
    b = new Foo( "world" );

b[Symbol.toStringTag] = "cool";

a.toString();               // [object Foo]
String( b );                // [object cool]

a instanceof Foo;           // true
b instanceof Foo;           // false

在原型(或?qū)嵗旧恚┥系?code>@@toStringTagsymbol指定一個用于[object ___]字符串化的字符串值寸齐。

@@hasInstancesymbol是一個在構(gòu)造器函數(shù)上的方法欲诺,它接收一個實例對象值并讓你通過放回truefalse來決定這個值是否應(yīng)當(dāng)被認(rèn)為是一個實例。

注意: 要在一個函數(shù)上設(shè)置@@hasInstance渺鹦,你必須使用Object.defineProperty(..)扰法,因為在Function.prototype上默認(rèn)的那一個是writable: false。更多信息參見本系列的 this與對象原型毅厚。

Symbol.species

在第三章的“類”中塞颁,我們介紹了@@speciessymbol,它控制一個類內(nèi)建的生成新實例的方法使用哪一個構(gòu)造器吸耿。

最常見的例子是祠锣,在子類化Array并且想要定義slice(..)之類被繼承的方法應(yīng)當(dāng)使用哪一個構(gòu)造器時。默認(rèn)地咽安,在一個Array的子類實例上調(diào)用的slice(..)將產(chǎn)生這個子類的實例伴网,坦白地說這正是你經(jīng)常希望的。

但是妆棒,你可以通過覆蓋一個類的默認(rèn)@@species定義來進(jìn)行元編程:

class Cool {
    // 將 `@@species` 倒推至被衍生的構(gòu)造器
    static get [Symbol.species]() { return this; }

    again() {
        return new this.constructor[Symbol.species]();
    }
}

class Fun extends Cool {}

class Awesome extends Cool {
    // 將 `@@species` 強(qiáng)制為父類構(gòu)造器
    static get [Symbol.species]() { return Cool; }
}

var a = new Fun(),
    b = new Awesome(),
    c = a.again(),
    d = b.again();

c instanceof Fun;           // true
d instanceof Awesome;       // false
d instanceof Cool;          // true

就像在前面的代碼段中的Cool的定義展示的那樣澡腾,在內(nèi)建的原生構(gòu)造器上的Symbol.species設(shè)定默認(rèn)為return this。它在用戶自己的類上沒有默認(rèn)值募逞,但也像展示的那樣蛋铆,這種行為很容易模擬。

如果你需要定義生成新實例的方法放接,使用new this.constructor[Symbol.species](..)的元編程模式刺啦,而不要用手寫的new this.constructor(..)或者new XYZ(..)。如此衍生的類就能夠自定義Symbol.species來控制哪一個構(gòu)造器來制造這些實例纠脾。

Symbol.toPrimitive

在本系列的 類型與文法 一書中玛瘸,我們討論了ToPrimitive抽象強(qiáng)制轉(zhuǎn)換操作苟蹈,它在對象為了某些操作(例如==比較或者+加法)而必須被強(qiáng)制轉(zhuǎn)換為一個基本類型值時被使用。在ES6以前宗兼,沒有辦法控制這個行為登钥。

在ES6中茉唉,在任意對象值上作為屬性的@@toPrimitivesymbol都可以通過指定一個方法來自定義這個ToPrimitive強(qiáng)制轉(zhuǎn)換。

考慮如下代碼:

var arr = [1,2,3,4,5];

arr + 10;               // 1,2,3,4,510

arr[Symbol.toPrimitive] = function(hint) {
    if (hint == "default" || hint == "number") {
        // 所有數(shù)字的和
        return this.reduce( function(acc,curr){
            return acc + curr;
        }, 0 );
    }
};

arr + 10;               // 25

Symbol.toPrimitive方法將根據(jù)調(diào)用ToPrimitive的操作期望何種類型结执,而被提供一個值為"string""number"艾凯,或"default"(這應(yīng)當(dāng)被解釋為"number")的 提示(hint)献幔。在前一個代碼段中,+加法操作沒有提示("default"將被傳遞)趾诗。一個*乘法操作將提示"number"蜡感,而一個String(arr)將提示"string"

警告: ==操作符將在一個對象上不使用任何提來示調(diào)用ToPrimitive操作 —— 如果存在@@toPrimitive方法的話恃泪,將使用"default"被調(diào)用 —— 如果另一個被比較的值不是一個對象郑兴。但是,如果兩個被比較的值都是對象贝乎,==的行為與===是完全相同的情连,也就是引用本身將被直接比較。這種情況下览效,@@toPrimitive根本不會被調(diào)用却舀。關(guān)于強(qiáng)制轉(zhuǎn)換和抽象操作的更多信息,參見本系列的 類型與文法锤灿。

正則表達(dá)式 Symbols

對于正則表達(dá)式對象挽拔,有四種通用 symbols 可以被覆蓋,它們控制著這些正則表達(dá)式在四個相應(yīng)的同名String.prototype函數(shù)中如何被使用:

  • @@match:一個正則表達(dá)式的Symbol.match值是使用被給定的正則表達(dá)式來匹配一個字符串值的全部或部分的方法但校。如果你為String.prototype.match(..)傳遞一個正則表達(dá)式做范例匹配螃诅,它就會被使用。

    匹配的默認(rèn)算法寫在ES6語言規(guī)范的第21.2.5.6部分(https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@match)胡野。你可以覆蓋這個默認(rèn)算法并提供額外的正則表達(dá)式特性璃哟,比如后顧斷言。

    Symbol.match還被用于isRegExp抽象操作(參見第六章的“字符串檢測函數(shù)”中的注意部分)來判定一個對象是否意在被用作正則表達(dá)式婴梧。為了使一個這樣的對象不被看作是正則表達(dá)式穗椅,可以將Symbol.match的值設(shè)置為false(或falsy的東西)強(qiáng)制這個檢查失敗辨绊。

  • @@replace:一個正則表達(dá)式的Symbol.replace值是被String.prototype.replace(..)使用的方法,來替換一個字符串里面出現(xiàn)的一個或所有字符序列匹表,這些字符序列匹配給出的正則表達(dá)式范例门坷。

    替換的默認(rèn)算法寫在ES6語言規(guī)范的第21.2.5.8部分(https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@replace)。

    一個覆蓋默認(rèn)算法的很酷的用法是提供額外的replacer可選參數(shù)值袍镀,比如通過用連續(xù)的替換值消費(fèi)可迭代對象來支持"abaca".replace(/a/g,[1,2,3])產(chǎn)生"1b2c3"默蚌。

  • @@search:一個正則表達(dá)式的Symbol.search值是被String.prototype.search(..)使用的方法,來在一個字符串中檢索一個匹配給定正則表達(dá)式的子字符串苇羡。

    檢索的默認(rèn)算法寫在ES6語言規(guī)范的第21.2.5.9部分(https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@search)绸吸。

  • @@split:一個正則表達(dá)式的Symbol.split值是被String.prototype.split(..)使用的方法,來將一個字符串在分隔符匹配給定正則表達(dá)式的位置分割為子字符串设江。

    分割的默認(rèn)算法寫在ES6語言規(guī)范的第21.2.5.11部分(https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@split)锦茁。

覆蓋內(nèi)建的正則表達(dá)式算法不是為心臟脆弱的人準(zhǔn)備的!JS帶有高度優(yōu)化的正則表達(dá)式引擎叉存,所以你自己的用戶代碼將很可能慢得多码俩。這種類型的元編程很精巧和強(qiáng)大,但是應(yīng)當(dāng)僅用于確實必要或有好處的情況下歼捏。

Symbol.isConcatSpreadable

@@isConcatSpreadablesymbol可以作為一個布爾屬性(Symbol.isConcatSpreadable)在任意對象上(比如一個數(shù)組或其他的可迭代對象)定義稿存,來指示當(dāng)它被傳遞給一個數(shù)組concat(..)時是否應(yīng)當(dāng)被 擴(kuò)散

考慮如下代碼:

var a = [1,2,3],
    b = [4,5,6];

b[Symbol.isConcatSpreadable] = false;

[].concat( a, b );      // [1,2,3,[4,5,6]]

Symbol.unscopables

@@unscopablessymbol可以作為一個對象屬性(Symbol.unscopables)在任意對象上定義瞳秽,來指示在一個with語句中哪一個屬性可以和不可以作為此法變量被暴露瓣履。

考慮如下代碼:

var o = { a:1, b:2, c:3 },
    a = 10, b = 20, c = 30;

o[Symbol.unscopables] = {
    a: false,
    b: true,
    c: false
};

with (o) {
    console.log( a, b, c );     // 1 20 3
}

一個在@@unscopables對象中的true指示這個屬性應(yīng)當(dāng)是 非作用域(unscopable) 的,因此會從此法作用域變量中被過濾掉练俐。false意味著它可以被包含在此法作用域變量中袖迎。

警告: with語句在strict模式下是完全禁用的,而且因此應(yīng)當(dāng)被認(rèn)為是在語言中被廢棄的痰洒。不要使用它瓢棒。更多信息參見本系列的 作用域與閉包。因為應(yīng)當(dāng)避免with丘喻,所以這個@@unscopablessymbol也是無意義的脯宿。

代理

在ES6中被加入的最明顯的元編程特性之一就是proxy特性。

一個代理是一種由你創(chuàng)建的特殊的對象泉粉,它“包”著另一個普通的對象 —— 或者說擋在這個普通對象的前面连霉。你可以在代理對象上注冊特殊的處理器(也叫 機(jī)關(guān)(traps))榴芳,當(dāng)對這個代理實施各種操作時被調(diào)用。這些處理器除了將操作 傳送 到原本的目標(biāo)/被包裝的對象上之外跺撼,還有機(jī)會運(yùn)行額外的邏輯窟感。

一個這樣的 機(jī)關(guān) 處理器的例子是,你可以在一個代理上定義一個攔截[[Get]]操作的get —— 它在當(dāng)你試圖訪問一個對象上的屬性時運(yùn)行歉井∈疗恚考慮如下代碼:

var obj = { a: 1 },
    handlers = {
        get(target,key,context) {
            // 注意:target === obj,
            // context === pobj
            console.log( "accessing: ", key );
            return Reflect.get(
                target, key, context
            );
        }
    },
    pobj = new Proxy( obj, handlers );

obj.a;
// 1

pobj.a;
// accessing: a
// 1

我們將一個get(..)處理器作為 處理器 對象的命名方法聲明(Proxy(..)的第二個參數(shù)值),它接收一個指向 目標(biāo) 對象的引用(obj)哩至,屬性的 名稱("a")躏嚎,和self/接受者/代理本身(pobj)。

在追蹤語句console.log(..)之后菩貌,我們通過Reflect.get(..)將操作“轉(zhuǎn)送”到obj卢佣。我們將在下一節(jié)詳細(xì)講解ReflectAPI,但要注意的是每個可用的代理機(jī)關(guān)都有一個相應(yīng)的同名Reflect函數(shù)箭阶。

這些映射是故意對稱的虚茶。每個代理處理器在各自的元編程任務(wù)實施時進(jìn)行攔截,而每個Reflect工具將各自的元編程任務(wù)在一個對象上實施仇参。每個代理處理器都有一個自動調(diào)用相應(yīng)Reflect工具的默認(rèn)定義嘹叫。幾乎可以肯定你將總是一前一后地使用ProxyReflect

這里的列表是你可以在一個代理上為一個 目標(biāo) 對象/函數(shù)定義的處理器诈乒,以及它們?nèi)绾?何時被觸發(fā):

  • get(..):通過[[Get]]待笑,在代理上訪問一個屬性(Reflect.get(..).屬性操作符或[ .. ]屬性操作符)
  • set(..):通過[[Set]]抓谴,在代理對象上設(shè)置一個屬性(Reflect.set(..)=賦值操作符寞缝,或者解構(gòu)賦值 —— 如果目標(biāo)是一個對象屬性的話)
  • deleteProperty(..):通過[[Delete]]癌压,在代理對象上刪除一個屬性 (Reflect.deleteProperty(..)delete)
  • apply(..)(如果 目標(biāo) 是一個函數(shù)):通過[[Call]],代理作為一個普通函數(shù)/方法被調(diào)用(Reflect.apply(..)荆陆,call(..)滩届,apply(..),或者(..)調(diào)用操作符)
  • construct(..)(如果 目標(biāo) 是一個構(gòu)造函數(shù)):通過[[Construct]]代理作為一個構(gòu)造器函數(shù)被調(diào)用(Reflect.construct(..)new
  • getOwnPropertyDescriptor(..):通過[[GetOwnProperty]]被啼,從代理取得一個屬性的描述符(Object.getOwnPropertyDescriptor(..)Reflect.getOwnPropertyDescriptor(..)
  • defineProperty(..):通過[[DefineOwnProperty]]帜消,在代理上設(shè)置一個屬性描述符(Object.defineProperty(..)Reflect.defineProperty(..)
  • getPrototypeOf(..):通過[[GetPrototypeOf]],取得代理的[[Prototype]]Object.getPrototypeOf(..)浓体,Reflect.getPrototypeOf(..)泡挺,__proto__, Object#isPrototypeOf(..),或instanceof
  • setPrototypeOf(..):通過[[SetPrototypeOf]]命浴,設(shè)置代理的[[Prototype]]Object.setPrototypeOf(..)娄猫,Reflect.setPrototypeOf(..)贱除,或__proto__
  • preventExtensions(..):通過[[PreventExtensions]]使代理成為不可擴(kuò)展的(Object.preventExtensions(..)Reflect.preventExtensions(..)
  • isExtensible(..):通過[[IsExtensible]],檢測代理的可擴(kuò)展性(Object.isExtensible(..)Reflect.isExtensible(..)
  • ownKeys(..):通過[[OwnPropertyKeys]]媳溺,取得一組代理的直屬屬性和/或直屬symbol屬性(Object.keys(..)月幌,Object.getOwnPropertyNames(..)Object.getOwnSymbolProperties(..)悬蔽,Reflect.ownKeys(..)扯躺,或JSON.stringify(..)
  • enumerate(..):通過[[Enumerate]],為代理的可枚舉直屬屬性及“繼承”屬性請求一個迭代器(Reflect.enumerate(..)for..in
  • has(..):通過[[HasProperty]]蝎困,檢測代理是否擁有一個直屬屬性或“繼承”屬性(Reflect.has(..)录语,Object#hasOwnProperty(..),或"prop" in obj

提示: 關(guān)于每個這些元編程任務(wù)的更多信息难衰,參見本章稍后的“Reflect API”一節(jié)钦无。

關(guān)于將會觸發(fā)各種機(jī)關(guān)的動作,除了在前面列表中記載的以外盖袭,一些機(jī)關(guān)還會由另一個機(jī)關(guān)的默認(rèn)動作間接地觸發(fā)失暂。舉例來說:

var handlers = {
        getOwnPropertyDescriptor(target,prop) {
            console.log(
                "getOwnPropertyDescriptor"
            );
            return Object.getOwnPropertyDescriptor(
                target, prop
            );
        },
        defineProperty(target,prop,desc){
            console.log( "defineProperty" );
            return Object.defineProperty(
                target, prop, desc
            );
        }
    },
    proxy = new Proxy( {}, handlers );

proxy.a = 2;
// getOwnPropertyDescriptor
// defineProperty

在設(shè)置一個屬性值時(不管是新添加還是更新),getOwnPropertyDescriptor(..)defineProperty(..)處理器被默認(rèn)的set(..)處理器觸發(fā)鳄虱。如果你還定義了你自己的set(..)處理器弟塞,你或許對context(不是target!)進(jìn)行了將會觸發(fā)這些代理機(jī)關(guān)的相應(yīng)調(diào)用拙已。

代理的限制

這些元編程處理器攔截了你可以對一個對象進(jìn)行的范圍很廣泛的一組基礎(chǔ)操作决记。但是,有一些操作不能(至少是還不能)被用于攔截倍踪。

例如系宫,從pobj代理到obj目標(biāo),這些操作全都沒有被攔截和轉(zhuǎn)送:

var obj = { a:1, b:2 },
    handlers = { .. },
    pobj = new Proxy( obj, handlers );

typeof obj;
String( obj );
obj + "";
obj == pobj;
obj === pobj

也許在未來建车,更多這些語言中的底層基礎(chǔ)操作都將是可攔截的扩借,那將給我們更多力量來從JavaScript自身擴(kuò)展它。

警告: 對于代理處理器的使用來說存在某些 不變量 —— 它們的行為不能被覆蓋缤至。例如潮罪,isExtensible(..)處理器的結(jié)果總是被強(qiáng)制轉(zhuǎn)換為一個boolean。這些不變量限制了一些你可以使用代理來自定義行為的能力领斥,但是它們這樣做只是為了防止你創(chuàng)建奇怪和不尋常(或不合邏輯)的行為嫉到。這些不變量的條件十分復(fù)雜,所以我們就不再這里全面闡述了月洛,但是這篇博文(http://www.2ality.com/2014/12/es6-proxies.html#invariants)很好地講解了它們何恶。

可撤銷的代理

一個一般的代理總是包裝著目標(biāo)對象,而且在創(chuàng)建之后就不能修改了 —— 只要保持著一個指向這個代理的引用嚼黔,代理的機(jī)制就將維持下去导而。但是忱叭,可能會有一些情況你想要創(chuàng)建一個這樣的代理:在你想要停止它作為代理時可以被停用。解決方案就是創(chuàng)建一個 可撤銷代理

var obj = { a: 1 },
    handlers = {
        get(target,key,context) {
            // 注意:target === obj,
            // context === pobj
            console.log( "accessing: ", key );
            return target[key];
        }
    },
    { proxy: pobj, revoke: prevoke } =
        Proxy.revocable( obj, handlers );

pobj.a;
// accessing: a
// 1

// 稍后:
prevoke();

pobj.a;
// TypeError

一個可撤銷代理是由Proxy.revocable(..)創(chuàng)建的今艺,它是一個普通的函數(shù)韵丑,不是一個像Proxy(..)那樣的構(gòu)造器。此外虚缎,它接收同樣的兩個參數(shù)值:目標(biāo)處理器撵彻。

new Proxy(..)不同的是,Proxy.revocable(..)的返回值不是代理本身实牡。取而代之的是陌僵,它返回一個帶有 proxyrevoke 兩個屬性的對象 —— 我們使用了對象解構(gòu)(參見第二章的“解構(gòu)”)來將這些屬性分別賦值給變量pobjprevoke

一旦可撤銷代理被撤銷创坞,任何訪問它的企圖(觸發(fā)它的任何機(jī)關(guān))都將拋出TypeError碗短。

一個使用可撤銷代理的例子可能是,將一個代理交給另一個存在于你應(yīng)用中题涨、并管理你模型中的數(shù)據(jù)的團(tuán)體偎谁,而不是給它們一個指向正式模型對象本身的引用。如果你的模型對象改變了或者被替換掉了纲堵,你希望廢除這個你交出去的代理巡雨,以便于其他的團(tuán)體能夠(通過錯誤!)知道要請求一個更新過的模型引用席函。

使用代理

這些代理處理器帶來的元編程的好處應(yīng)當(dāng)是顯而易見的铐望。我們可以全面地攔截(而因此覆蓋)對象的行為,這意味著我們可以用一些非常強(qiáng)大的方式將對象行為擴(kuò)展至JS核心之外茂附。我們將看幾個模式的例子來探索這些可能性正蛙。

代理前置,代理后置

正如我們早先提到過的营曼,你通常將一個代理考慮為一個目標(biāo)對象的“包裝”跟畅。在這種意義上,代理就變成了代碼接口所針對的主要對象溶推,而實際的目標(biāo)對象則保持被隱藏/被保護(hù)的狀態(tài)。

你可能這么做是因為你希望將對象傳遞到某個你不能完全“信任”的地方去奸攻,如此你需要在它的訪問權(quán)上強(qiáng)制實施一些特殊的規(guī)則蒜危,而不是傳遞這個對象本身。

考慮如下代碼:

var messages = [],
    handlers = {
        get(target,key) {
            // 是字符串值嗎睹耐?
            if (typeof target[key] == "string") {
                // 過濾掉標(biāo)點(diǎn)符號
                return target[key]
                    .replace( /[^\w]/g, "" );
            }

            // 讓其余的東西通過
            return target[key];
        },
        set(target,key,val) {
            // 僅設(shè)置唯一的小寫字符串
            if (typeof val == "string") {
                val = val.toLowerCase();
                if (target.indexOf( val ) == -1) {
                    target.push(
                        val.toLowerCase()
                    );
                }
            }
            return true;
        }
    },
    messages_proxy =
        new Proxy( messages, handlers );

// 在別處:
messages_proxy.push(
    "heLLo...", 42, "wOrlD!!", "WoRld!!"
);

messages_proxy.forEach( function(val){
    console.log(val);
} );
// hello world

messages.forEach( function(val){
    console.log(val);
} );
// hello... world!!

我稱此為 代理前置 設(shè)計辐赞,因為我們首先(主要、完全地)與代理進(jìn)行互動硝训。

我們在與messages_proxy的互動上強(qiáng)制實施了一些特殊規(guī)則响委,這些規(guī)則不會強(qiáng)制實施在messages本身上新思。我們僅在值是一個不重復(fù)的字符串時才將它添加為元素;我們還將這個值變?yōu)樾懽阜纭.?dāng)從messages_proxy取得值時夹囚,我們過濾掉字符串中所有的標(biāo)點(diǎn)符號。

另一種方式是邀窃,我們可以完全反轉(zhuǎn)這個模式荸哟,讓目標(biāo)與代理交互而不是讓代理與目標(biāo)交互。這樣瞬捕,代碼其實只與主對象交互鞍历。達(dá)成這種后備方案的最簡單的方法是,讓代理對象存在于主對象的[[Prototype]]鏈中肪虎。

考慮如下代碼:

var handlers = {
        get(target,key,context) {
            return function() {
                context.speak(key + "!");
            };
        }
    },
    catchall = new Proxy( {}, handlers ),
    greeter = {
        speak(who = "someone") {
            console.log( "hello", who );
        }
    };

// 讓 `catchall` 成為 `greeter` 的后備方法
Object.setPrototypeOf( greeter, catchall );

greeter.speak();                // hello someone
greeter.speak( "world" );       // hello world

greeter.everyone();             // hello everyone!

我們直接與greeter而非catchall進(jìn)行交互劣砍。當(dāng)我們調(diào)用speak(..)時,它在greeter上被找到并直接使用扇救。但當(dāng)我們試圖訪問everyone()這樣的方法時刑枝,這個函數(shù)并不存在于greeter

默認(rèn)的對象屬性行為是向上檢查[[Prototype]]鏈(參見本系列的 this與對象原型)爵政,所以catchall被詢問有沒有一個everyone屬性仅讽。然后代理的get()處理器被調(diào)用并返回一個函數(shù),這個函數(shù)使用被訪問的屬性名("everyone")調(diào)用speak(..)钾挟。

我稱這種模式為 代理后置洁灵,因為代理僅被用作最后一道防線。

"No Such Property/Method"

一個關(guān)于JS的常見的抱怨是掺出,在你試著訪問或設(shè)置一個對象上還不存在的屬性時徽千,默認(rèn)情況下對象不是非常具有防御性。你可能希望為一個對象預(yù)定義所有這些屬性/方法汤锨,而且在后續(xù)使用不存在的屬性名時拋出一個錯誤双抽。

我們可以使用一個代理來達(dá)成這種想法,既可以使用 代理前置 也可以 代理后置 設(shè)計闲礼。我們將兩者都考慮一下牍汹。

var obj = {
        a: 1,
        foo() {
            console.log( "a:", this.a );
        }
    },
    handlers = {
        get(target,key,context) {
            if (Reflect.has( target, key )) {
                return Reflect.get(
                    target, key, context
                );
            }
            else {
                throw "No such property/method!";
            }
        },
        set(target,key,val,context) {
            if (Reflect.has( target, key )) {
                return Reflect.set(
                    target, key, val, context
                );
            }
            else {
                throw "No such property/method!";
            }
        }
    },
    pobj = new Proxy( obj, handlers );

pobj.a = 3;
pobj.foo();         // a: 3

pobj.b = 4;         // Error: No such property/method!
pobj.bar();         // Error: No such property/method!

對于get(..)set(..)兩者,我們僅在目標(biāo)對象的屬性已經(jīng)存在時才轉(zhuǎn)送操作柬泽;否則拋出錯誤慎菲。代理對象應(yīng)當(dāng)是進(jìn)行交互的主對象,因為它攔截這些操作來提供保護(hù)锨并。

現(xiàn)在露该,讓我們考慮一下反過來的 代理后置 設(shè)計:

var handlers = {
        get() {
            throw "No such property/method!";
        },
        set() {
            throw "No such property/method!";
        }
    },
    pobj = new Proxy( {}, handlers ),
    obj = {
        a: 1,
        foo() {
            console.log( "a:", this.a );
        }
    };

// 讓 `pobj` 稱為 `obj` 的后備
Object.setPrototypeOf( obj, pobj );

obj.a = 3;
obj.foo();          // a: 3

obj.b = 4;          // Error: No such property/method!
obj.bar();          // Error: No such property/method!

在處理器如何定義的角度上,這里的 代理后置 設(shè)計相當(dāng)簡單第煮。與攔截[[Get]][[Set]]操作并僅在目標(biāo)屬性存在時轉(zhuǎn)送它們不同解幼,我們依賴于這樣一個事實:不管[[Get]]還是[[Set]]到達(dá)了我們的pobj后備對象抑党,這個動作已經(jīng)遍歷了整個[[Prototype]]鏈并且沒有找到匹配的屬性。在這時我們可以自由地撵摆、無條件地拋出錯誤底靠。很酷,對吧台汇?

代理黑入 [[Prototype]]

[[Get]]操作是[[Prototype]]機(jī)制被調(diào)用的主要渠道苛骨。當(dāng)一個屬性不能在直接對象上找到時,[[Get]]會自動將操作交給[[Prototype]]對象苟呐。

這意味著你可以使用一個代理的get(..)機(jī)關(guān)來模擬或擴(kuò)展這個[[Prototype]]機(jī)制的概念痒芝。

我們將考慮的第一種黑科技是創(chuàng)建兩個通過[[Prototype]]循環(huán)鏈接的對象(或者說,至少看起來是這樣GK亍)严衬。你不能實際創(chuàng)建一個真正循環(huán)的[[Prototype]]鏈,因為引擎將會拋出一個錯誤笆呆。但是代理可以假冒它请琳!

考慮如下代碼:

var handlers = {
        get(target,key,context) {
            if (Reflect.has( target, key )) {
                return Reflect.get(
                    target, key, context
                );
            }
            // 假冒循環(huán)的 `[[Prototype]]`
            else {
                return Reflect.get(
                    target[
                        Symbol.for( "[[Prototype]]" )
                    ],
                    key,
                    context
                );
            }
        }
    },
    obj1 = new Proxy(
        {
            name: "obj-1",
            foo() {
                console.log( "foo:", this.name );
            }
        },
        handlers
    ),
    obj2 = Object.assign(
        Object.create( obj1 ),
        {
            name: "obj-2",
            bar() {
                console.log( "bar:", this.name );
                this.foo();
            }
        }
    );

// 假冒循環(huán)的 `[[Prototype]]` 鏈
obj1[ Symbol.for( "[[Prototype]]" ) ] = obj2;

obj1.bar();
// bar: obj-1 <-- 通過代理假冒 [[Prototype]]
// foo: obj-1 <-- `this` 上下文環(huán)境依然被保留

obj2.foo();
// foo: obj-2 <-- 通過 [[Prototype]]

注意: 為了讓事情簡單一些,在這個例子中我們沒有代理/轉(zhuǎn)送[[Set]]赠幕。要完整地模擬[[Prototype]]兼容俄精,你會想要實現(xiàn)一個set(..)處理器,它在[[Prototype]]鏈上檢索一個匹配得屬性并遵循它的描述符的行為(例如榕堰,set竖慧,可寫性)。參見本系列的 this與對象原型逆屡。

在前面的代碼段中圾旨,obj2憑借Object.create(..)語句[[Prototype]]鏈接到obj1。但是要創(chuàng)建反向(循環(huán))的鏈接魏蔗,我們在obj1的symbol位置Symbol.for("[[Prototype]]")(參見第二章的“Symbol”)上創(chuàng)建了一個屬性砍的。這個symbol可能看起來有些特別/魔幻,但它不是的莺治。它只是允許我使用一個被方便地命名的屬性廓鞠,這個屬性在語義上看來是與我進(jìn)行的任務(wù)有關(guān)聯(lián)的。

然后谣旁,代理的get(..)處理器首先檢查一個被請求的key是否存在于代理上床佳。如果每個有,操作就被手動地交給存儲在targetSymbol.for("[[Prototype]]")位置中的對象引用蔓挖。

這種模式的一個重要優(yōu)點(diǎn)是,在obj1obj2之間建立循環(huán)關(guān)系幾乎沒有入侵它們的定義馆衔。雖然前面的代碼段為了簡短而將所有的步驟交織在一起瘟判,但是如果你仔細(xì)觀察怨绣,代理處理器的邏輯完全是范用的(不具體地知道obj1obj2)。所以拷获,這段邏輯可以抽出到一個簡單的將它們連在一起的幫助函數(shù)中篮撑,例如setCircularPrototypeOf(..)。我們將此作為一個練習(xí)留給讀者匆瓜。

現(xiàn)在我們看到了如何使用get(..)來模擬一個[[Prototype]]鏈接赢笨,但讓我們將這種黑科技推動的遠(yuǎn)一些。與其制造一個循環(huán)[[Prototype]]驮吱,搞一個多重[[Prototype]]鏈接(也就是“多重繼承”)怎么樣茧妒?這看起來相當(dāng)直白:

var obj1 = {
        name: "obj-1",
        foo() {
            console.log( "obj1.foo:", this.name );
        },
    },
    obj2 = {
        name: "obj-2",
        foo() {
            console.log( "obj2.foo:", this.name );
        },
        bar() {
            console.log( "obj2.bar:", this.name );
        }
    },
    handlers = {
        get(target,key,context) {
            if (Reflect.has( target, key )) {
                return Reflect.get(
                    target, key, context
                );
            }
            // 假冒多重 `[[Prototype]]`
            else {
                for (var P of target[
                    Symbol.for( "[[Prototype]]" )
                ]) {
                    if (Reflect.has( P, key )) {
                        return Reflect.get(
                            P, key, context
                        );
                    }
                }
            }
        }
    },
    obj3 = new Proxy(
        {
            name: "obj-3",
            baz() {
                this.foo();
                this.bar();
            }
        },
        handlers
    );

// 假冒多重 `[[Prototype]]` 鏈接
obj3[ Symbol.for( "[[Prototype]]" ) ] = [
    obj1, obj2
];

obj3.baz();
// obj1.foo: obj-3
// obj2.bar: obj-3

注意: 正如在前面的循環(huán)[[Prototype]]例子后的注意中提到的,我們沒有實現(xiàn)set(..)處理器左冬,但對于一個將[[Set]]模擬為普通[[Prototype]]行為的解決方案來說桐筏,它將是必要的。

obj3被設(shè)置為多重委托到obj1obj2拇砰。在obj2.baz()中梅忌,this.foo()調(diào)用最終成為從obj1中抽出foo()(先到先得,雖然還有一個在obj2上的foo())除破。如果我們將連接重新排列為obj2, obj1牧氮,那么obj2.foo()將被找到并使用。

同理瑰枫,this.bar()調(diào)用沒有在obj1上找到bar()踱葛,所以它退而檢查obj2,這里找到了一個匹配躁垛。

obj1obj2代表obj3的兩個平行的[[Prototype]]鏈剖毯。obj1和/或obj2自身可以擁有委托至其他對象的普通[[Prototype]],或者自身也可以是多重委托的代理(就像obj3一樣)教馆。

正如先前的循環(huán)[[Prototype]]的例子一樣逊谋,obj1obj2obj3的定義幾乎完全與處理多重委托的范用代理邏輯相分離土铺。定義一個setPrototypesOf(..)(注意那個“s”=鹤獭)這樣的工具將是小菜一碟,它接收一個主對象和一組模擬多重[[Prototype]]鏈接用的對象悲敷。同樣究恤,我們將此作為練習(xí)留給讀者。

希望在這種種例子之后代理的力量現(xiàn)在變得明朗了后德。代理使得許多強(qiáng)大的元編程任務(wù)成為可能部宿。

Reflect API

Reflect對象是一個普通對象(就像Math),不是其他內(nèi)建原生類型那樣的函數(shù)/構(gòu)造器。

它持有對應(yīng)于你可以控制的各種元編程任務(wù)的靜態(tài)函數(shù)理张。這些函數(shù)與代理可以定義的處理器方法(機(jī)關(guān))一一對應(yīng)赫蛇。

這些函數(shù)中的一些看起來與在Object上的同名函數(shù)很相似:

  • Reflect.getOwnPropertyDescriptor(..)
  • Reflect.defineProperty(..)
  • Reflect.getPrototypeOf(..)
  • Reflect.setPrototypeOf(..)
  • Reflect.preventExtensions(..)
  • Reflect.isExtensible(..)

這些工具一般與它們的Object.*對等物的行為相同。但一個區(qū)別是雾叭,Object.*對等物在它們的第一個參數(shù)值(目標(biāo)對象)還不是對象的情況下悟耘,試圖將它強(qiáng)制轉(zhuǎn)換為一個對象。Reflect.*方法在同樣的情況下僅簡單地拋出一個錯誤织狐。

一個對象的鍵可以使用這些工具訪問/檢測:

  • Reflect.ownKeys(..):返回一個所有直屬(不是“繼承的”)鍵的列表暂幼,正如被 Object.getOwnPropertyNames(..)Object.getOwnPropertySymbols(..)返回的那樣。關(guān)于鍵的順序問題移迫,參見“屬性枚舉順序”一節(jié)旺嬉。
  • Reflect.enumerate(..):返回一個產(chǎn)生所有(直屬和“繼承的”)非symbol、可枚舉的鍵的迭代器(參見本系列的 this與對象原型)起意。 實質(zhì)上鹰服,這組鍵與在for..in循環(huán)中被處理的那一組鍵是相同的。關(guān)于鍵的順序問題揽咕,參見“屬性枚舉順序”一節(jié)悲酷。
  • Reflect.has(..):實質(zhì)上與用于檢查一個屬性是否存在于一個對象或它的[[Prototype]]鏈上的in操作符相同。例如亲善,Reflect.has(o,"foo")實質(zhì)上實施"foo" in o肮塞。

函數(shù)調(diào)用和構(gòu)造器調(diào)用可以使用這些工具手動地實施犯建,與普通的語法(例如夫啊,(..)new)分開:

  • Reflect.apply(..):例如遮晚,Reflect.apply(foo,thisObj,[42,"bar"])使用thisObj作為foo(..)函數(shù)的this來調(diào)用它,并傳入?yún)?shù)值42"bar"渣蜗。
  • Reflect.construct(..):例如屠尊,Reflect.construct(foo,[42,"bar"])實質(zhì)上調(diào)用new foo(42,"bar")

對象屬性訪問耕拷,設(shè)置讼昆,和刪除可以使用這些工具手動實施:

  • Reflect.get(..):例如,Reflect.get(o,"foo")會取得o.foo骚烧。
  • Reflect.set(..):例如浸赫,Reflect.set(o,"foo",42)實質(zhì)上實施o.foo = 42
  • Reflect.deleteProperty(..):例如赃绊,Reflect.deleteProperty(o,"foo")實質(zhì)上實施delete o.foo既峡。

Reflect的元編程能力給了你可以模擬各種語法特性的程序化等價物,暴露以前隱藏著的抽象操作碧查。例如运敢,你可以使用這些能力來擴(kuò)展 領(lǐng)域特定語言(DSL)的特性和API。

屬性順序

在ES6之前,羅列一個對象的鍵/屬性的順序沒有在語言規(guī)范中定義传惠,而是依賴于具體實現(xiàn)的肤视。一般來說,大多數(shù)引擎會以創(chuàng)建的順序來羅列它們涉枫,雖然開發(fā)者們已經(jīng)被強(qiáng)烈建議永遠(yuǎn)不要依仗這種順序。

在ES6中腐螟,羅列直屬屬性的屬性是由[[OwnPropertyKeys]]算法定義的(ES6語言規(guī)范愿汰,9.1.12部分),它產(chǎn)生所有直屬屬性(字符串或symbol)乐纸,不論其可枚舉性衬廷。這種順序僅對Reflect.ownKeys(..)有保證()。

這個順序是:

  1. 首先汽绢,以數(shù)字上升的順序吗跋,枚舉所有數(shù)字索引的直屬屬性。
  2. 然后宁昭,以創(chuàng)建順序枚舉剩下的直屬字符串屬性名跌宛。
  3. 最后,以創(chuàng)建順序枚舉直屬symbol屬性积仗。

考慮如下代碼:

var o = {};

o[Symbol("c")] = "yay";
o[2] = true;
o[1] = true;
o.b = "awesome";
o.a = "cool";

Reflect.ownKeys( o );               // [1,2,"b","a",Symbol(c)]
Object.getOwnPropertyNames( o );    // [1,2,"b","a"]
Object.getOwnPropertySymbols( o );  // [Symbol(c)]

另一方面疆拘,[[Enumeration]]算法(ES6語言規(guī)范,9.1.11部分)從目標(biāo)對象和它的[[Prototype]]鏈中僅產(chǎn)生可枚舉屬性寂曹。它被用于Reflect.enumerate(..)for..in哎迄。可觀察到的順序是依賴于具體實現(xiàn)的隆圆,語言規(guī)范沒有控制它漱挚。

相比之下,Object.keys(..)調(diào)用[[OwnPropertyKeys]]算法來得到一個所有直屬屬性的列表渺氧。但是旨涝,它過濾掉了不可枚舉屬性,然后特別為了JSON.stringify(..)for..in而將這個列表重排阶女,以匹配遺留的颊糜、依賴于具體實現(xiàn)的行為。所以通過擴(kuò)展秃踩,這個順序 Reflect.enumerate(..)的順序像吻合衬鱼。

換言之,所有四種機(jī)制(Reflect.enumerate(..)憔杨,Object.keys(..)鸟赫,for..in,和JSON.stringify(..))都同樣將與依賴于具體實現(xiàn)的順序像吻合,雖然技術(shù)上它們是以不同的方式達(dá)到的同樣的效果抛蚤。

具體實現(xiàn)可以將這四種機(jī)制與[[OwnPropertyKeys]]的順序相吻合台谢,但不是必須的。無論如何岁经,你將很可能從它們的行為中觀察到以下的排序:

var o = { a: 1, b: 2 };
var p = Object.create( o );
p.c = 3;
p.d = 4;

for (var prop of Reflect.enumerate( p )) {
    console.log( prop );
}
// c d a b

for (var prop in p) {
    console.log( prop );
}
// c d a b

JSON.stringify( p );
// {"c":3,"d":4}

Object.keys( p );
// ["c","d"]

這一切可以歸納為:在ES6中朋沮,根據(jù)語言規(guī)范Reflect.ownKeys(..)Object.getOwnPropertyNames(..)缀壤,和Object.getOwnPropertySymbols(..)保證都有可預(yù)見和可靠的順序樊拓。所以依賴于這種順序來建造代碼是安全的。

Reflect.enumerate(..)塘慕,Object.keys(..)筋夏,和for..in (擴(kuò)展一下的話還有JSON.stringification(..))繼續(xù)互相共享一個可觀察的順序,就像它們往常一樣图呢。但這個順序不一定與Reflect.ownKeys(..)的相同条篷。在使用它們依賴于具體實現(xiàn)的順序時依然應(yīng)當(dāng)小心。

特性測試

什么是特性測試蛤织?它是一種由你運(yùn)行來判定一個特性是否可用的測試赴叹。有些時候,這種測試不僅是為了判定存在性指蚜,還是為判定對特定行為的適應(yīng)性 —— 特性可能存在但有bug稚瘾。

這是一種元編程技術(shù) —— 測試你程序?qū)⒁\(yùn)行的環(huán)境然后判定你的程序應(yīng)當(dāng)如何動作。

在JS中特性測試最常見的用法是檢測一個API的存在性姚炕,而且如果它不存在摊欠,就定義一個填補(bǔ)(見第一章)。例如:

if (!Number.isNaN) {
    Number.isNaN = function(x) {
        return x !== x;
    };
}

在這個代碼段中的if語句就是一個元編程:我們探測我們的程序和它的運(yùn)行時環(huán)境柱宦,來判定我們是否和如何進(jìn)行后續(xù)處理些椒。

但是如何測試一個涉及新語法的特性呢?

你可能會嘗試這樣的東西:

try {
    a = () => {};
    ARROW_FUNCS_ENABLED = true;
}
catch (err) {
    ARROW_FUNCS_ENABLED = false;
}

不幸的是掸刊,這不能工作免糕,因為我們的JS程序是要被編譯的。因此忧侧,如果引擎還沒有支持ES6箭頭函數(shù)的話石窑,它就會在() => {}語法的地方熄火。你程序中的語法錯誤會阻止它的運(yùn)行蚓炬,進(jìn)而阻止你程序根據(jù)特性是否被支持而進(jìn)行后續(xù)的不同相應(yīng)松逊。

為了圍繞語法相關(guān)的特性進(jìn)行特性測試的元編程,我們需要一個方法將測試與我們程序?qū)⒁ㄟ^的初始編譯步驟隔離開肯夏。舉例來說经宏,如果我們能夠?qū)⑦M(jìn)行測試的代碼存儲在一個字符串中犀暑,之后JS引擎默認(rèn)地將不會嘗試編譯這個字符串中的內(nèi)容,直到我們要求它這么做烁兰。

你的思路是不是跳到了使用eval(..)耐亏?

別這么著急』φ澹看看本系列的 作用域與閉包 來了解一下為什么eval(..)是一個壞主意广辰。但是有另外一個缺陷較少的選項:Function(..)構(gòu)造器。

考慮如下代碼:

try {
    new Function( "( () => {} )" );
    ARROW_FUNCS_ENABLED = true;
}
catch (err) {
    ARROW_FUNCS_ENABLED = false;
}

好了主之,現(xiàn)在我們判定一個像箭頭函數(shù)這樣的特性是否 被當(dāng)前的引擎所編譯來進(jìn)行元編程轨域。你可能會想知道,我們要用這種信息做什么杀餐?

檢查API的存在性,并定義后備的API填補(bǔ)朱巨,對于特性檢測成功或失敗來說都是一條明確的道路史翘。但是對于從ARROW_FUNCS_ENABLEDtrue還是false中得到的信息來說,我們能對它做什么呢冀续?

因為如果引擎不支持一種特性琼讽,它的語法就不能出現(xiàn)在一個文件中,所以你不能在這個文件中定義使用這種語法的函數(shù)洪唐。

你所能做的是钻蹬,使用測試來判定你應(yīng)當(dāng)加載哪一組JS文件。例如凭需,如果在你的JS應(yīng)用程序中的啟動裝置中有一組這樣的特性測試问欠,那么它就可以測試環(huán)境來判定你的ES6代碼是否可以直接加載運(yùn)行,或者你是否需要加載一個代碼的轉(zhuǎn)譯版本(參見第一章)粒蜈。

這種技術(shù)稱為 分割投遞顺献。

事實表明,你使用ES6編寫的JS程序有時可以在ES6+瀏覽器中完全“原生地”運(yùn)行枯怖,但是另一些時候需要在前ES6瀏覽器中運(yùn)行轉(zhuǎn)譯版本注整。如果你總是加載并使用轉(zhuǎn)譯代碼,即便是在新的ES6兼容環(huán)境中度硝,至少是有些情況下你運(yùn)行的也是次優(yōu)的代碼肿轨。這并不理想。

分割投遞更加復(fù)雜和精巧蕊程,但對于你編寫的代碼和你的程序所必須在其中運(yùn)行的瀏覽器支持的特性之間椒袍,它代表一種更加成熟和健壯的橋接方式。

FeatureTests.io

為所有的ES6+語法以及語義行為定義特性測試藻茂,是一項你可能不想自己解決的艱巨任務(wù)槐沼。因為這些測試要求動態(tài)編譯(new Function(..))曙蒸,這會產(chǎn)生不幸的性能損耗。

另外岗钩,在每次你的應(yīng)用運(yùn)行時都執(zhí)行這些測試可能是一種浪費(fèi)纽窟,因為平均來說一個用戶的瀏覽器在幾周之內(nèi)至多只會更新一次,而即使是這樣兼吓,新特性也不一定會在每次更新中都出現(xiàn)臂港。

最終,管理一個對你特定代碼庫進(jìn)行的特性測試列表 —— 你的程序?qū)⒑苌儆玫紼S6的全部 —— 是很容易失控而且易錯的视搏。

https://featuretests.io”的“特性測試服務(wù)”為這種挫折提供了解決方案审孽。

你可以將這個服務(wù)的庫加載到你的頁面中,而它會加載最新的測試定義并運(yùn)行所有的特性測試浑娜。在可能的情況下佑力,它將使用Web Worker的后臺處理中這樣做,以降低性能上的開銷筋遭。它還會使用LocalStorage持久化來緩存測試的結(jié)果 —— 以一種可以被所有你訪問的使用這個服務(wù)的站點(diǎn)所共享的方式打颤,這將及大地降低測試需要在每個瀏覽器實例上運(yùn)行的頻度。

你可以在每一個用戶的瀏覽器上進(jìn)行運(yùn)行時特性測試漓滔,而且你可以使用這些測試結(jié)果動態(tài)地向用戶傳遞最適合他們環(huán)境的代碼(不多也不少)编饺。

另外,這個服務(wù)還提供工具和API來掃描你的文件以判定你需要什么特性响驴,這樣你就能夠完全自動化你的分割投遞構(gòu)建過程透且。

對ES6的所有以及未來的部分進(jìn)行特性測試,以確保對于任何給定的環(huán)境都只有最佳的代碼會被加載和運(yùn)行 —— FeatureTests.io使這成為可能豁鲤。

尾部調(diào)用優(yōu)化(TCO)

通常來說秽誊,當(dāng)從一個函數(shù)內(nèi)部發(fā)起對另一個函數(shù)的調(diào)用時,就會分配一個 棧幀 來分離地管理這另一個函數(shù)調(diào)用的變量/狀態(tài)琳骡。這種分配不僅花費(fèi)一些處理時間养距,還會消耗一些額外的內(nèi)存。

一個調(diào)用棧鏈從一個函數(shù)到另一個再到另一個日熬,通常至多擁有10-15跳棍厌。在這些場景下,內(nèi)存使用不太可能是某種實際問題竖席。

然而耘纱,當(dāng)你考慮遞歸編程(一個函數(shù)頻繁地調(diào)用自己) —— 或者使用兩個或更多的函數(shù)相互調(diào)用而構(gòu)成相互遞歸 —— 調(diào)用棧就可能輕易地到達(dá)上百,上千毕荐,或更多層的深度束析。如果內(nèi)存的使用無限制地增長下去,你可能看到了它將導(dǎo)致的問題憎亚。

JavaScript引擎不得不設(shè)置一個隨意的限度來防止這樣的編程技術(shù)耗盡瀏覽器或設(shè)備的內(nèi)存员寇。這就是為什么我們會在到達(dá)這個限度時得到令人沮喪的“RangeError: Maximum call stack size exceeded”弄慰。

警告: 調(diào)用棧深度的限制是不由語言規(guī)范控制的。它是依賴于具體實現(xiàn)的蝶锋,而且將會根據(jù)瀏覽器和設(shè)備不同而不同陆爽。你絕不應(yīng)該帶著可精確觀察到的限度的強(qiáng)烈臆想進(jìn)行編碼,因為它們還很可能在每個版本中變化扳缕。

一種稱為 尾部調(diào)用 的特定函數(shù)調(diào)用模式慌闭,可以以一種避免額外的棧幀分配的方法進(jìn)行優(yōu)化。如果額外的分配可以被避免躯舔,那么就沒有理由隨意地限制調(diào)用棧的深度驴剔,這樣引擎就可以讓它們沒有邊界地運(yùn)行下去。

一個尾部調(diào)用是一個帶有函數(shù)調(diào)用的return語句粥庄,除了返回它的值丧失,函數(shù)調(diào)用之后沒有任何事情需要發(fā)生。

這種優(yōu)化只能在strict模式下進(jìn)行惜互。又一個你總是應(yīng)該用strict編寫所有代碼的理由布讹!

這個函數(shù)調(diào)用 不是 在尾部:

"use strict";

function foo(x) {
    return x * 2;
}

function bar(x) {
    // 不是一個尾部調(diào)用
    return 1 + foo( x );
}

bar( 10 );              // 21

foo(x)調(diào)用完成后必須進(jìn)行1 + ..,所以那個bar(..)調(diào)用的狀態(tài)需要被保留载佳。

但是下面的代碼段中展示的foo(..)bar(..)都是位于尾部,因為它們都是在自身代碼路徑上(除了return以外)發(fā)生的最后一件事:

"use strict";

function foo(x) {
    return x * 2;
}

function bar(x) {
    x = x + 1;
    if (x > 10) {
        return foo( x );
    }
    else {
        return bar( x + 1 );
    }
}

bar( 5 );               // 24
bar( 15 );              // 32

在這個程序中臀栈,bar(..)明顯是遞歸蔫慧,但foo(..)只是一個普通的函數(shù)調(diào)用。這兩個函數(shù)調(diào)用都位于 恰當(dāng)?shù)奈膊课恢?/em>权薯。x + 1bar(..)調(diào)用之前被求值姑躲,而且不論這個調(diào)用何時完成,所有將要放生的只有return盟蚣。

這些形式的恰當(dāng)尾部調(diào)用(Proper Tail Calls —— PTC)是可以被優(yōu)化的 —— 稱為尾部調(diào)用優(yōu)化(TCO)—— 于是額外的棧幀分配是不必要的黍析。與為下一個函數(shù)調(diào)用創(chuàng)建新的棧幀不同,引擎會重用既存的棧幀屎开。這能夠工作是因為一個函數(shù)不需要保留任何當(dāng)前狀態(tài) —— 在PTC之后的狀態(tài)下不會發(fā)生任何事情阐枣。

TCO意味著調(diào)用棧可以有多深實際上是沒有限度的奄抽。這種技巧稍稍改進(jìn)了一般程序中的普通函數(shù)調(diào)用蔼两,但更重要的是它打開了一扇大門:可以使用遞歸表達(dá)程序,即使它的調(diào)用棧深度有成千上萬層逞度。

我們不再局限于單純地在理論上考慮用遞歸解決問題了额划,而是可以在真實的JavaScript程序中使用它!

作為ES6档泽,所有的PTC都應(yīng)該是可以以這種方式優(yōu)化的俊戳,不論遞歸與否揖赴。

重寫尾部調(diào)用

然而,障礙是只有PTC是可以被優(yōu)化的抑胎;非PTC理所當(dāng)然地依然可以工作燥滑,但是將造成往常那樣的棧幀分配。如果你希望優(yōu)化機(jī)制啟動圆恤,就必須小心地使用PTC構(gòu)造你的函數(shù)突倍。

如果你有一個沒有用PTC編寫的函數(shù),你可能會發(fā)現(xiàn)你需要手動地重新安排你的代碼盆昙,使它成為合法的TCO羽历。

考慮如下代碼:

"use strict";

function foo(x) {
    if (x <= 1) return 1;
    return (x / 2) + foo( x - 1 );
}

foo( 123456 );          // RangeError

foo(x-1)的調(diào)用不是一個PTC,因為在return之前它的結(jié)果必須被加上(x / 2)淡喜。

但是秕磷,要使這段代碼在一個ES6引擎中是合法的TCO,我們可以像下面這樣重寫它:

"use strict";

var foo = (function(){
    function _foo(acc,x) {
        if (x <= 1) return acc;
        return _foo( (x / 2) + acc, x - 1 );
    }

    return function(x) {
        return _foo( 1, x );
    };
})();

foo( 123456 );          // 3810376848.5

如果你在一個實現(xiàn)了TCO的ES6引擎中運(yùn)行前面這個代碼段炼团,你將會如展示的那樣得到答案3810376848.5澎嚣。然而,它仍然會在非TCO引擎中因為RangeError而失敗瘟芝。

非TCO優(yōu)化

有另一種技術(shù)可以重寫代碼易桃,讓調(diào)用棧不隨每次調(diào)用增長。

一個這樣的技術(shù)稱為 蹦床锌俱,它相當(dāng)于讓每一部分結(jié)果表示為一個函數(shù)晤郑,這個函數(shù)要么返回另一個部分結(jié)果函數(shù),要么返回最終結(jié)果贸宏。然后你就可以簡單地循環(huán)直到你不再收到一個函數(shù)造寝,這時你就得到了結(jié)果】粤罚考慮如下代碼:

"use strict";

function trampoline( res ) {
    while (typeof res == "function") {
        res = res();
    }
    return res;
}

var foo = (function(){
    function _foo(acc,x) {
        if (x <= 1) return acc;
        return function partial(){
            return _foo( (x / 2) + acc, x - 1 );
        };
    }

    return function(x) {
        return trampoline( _foo( 1, x ) );
    };
})();

foo( 123456 );          // 3810376848.5

這種返工需要一些最低限度的改變來將遞歸抽出到trampoline(..)中的循環(huán)中:

  1. 首先诫龙,我們將return _foo ..這一行包裝進(jìn)函數(shù)表達(dá)式return partial() {..
  2. 然后我們將_foo(1,x)包裝進(jìn)trampoline(..)調(diào)用鲫咽。

這種技術(shù)之所以不受調(diào)用棧限制的影響签赃,是因為每個內(nèi)部的partial(..)函數(shù)都只是返回到trampoline(..)while循環(huán)中,這個循環(huán)運(yùn)行它然后再一次循環(huán)迭代分尸。換言之姊舵,partial(..)并不遞歸地調(diào)用它自己,它只是返回另一個函數(shù)寓落。棧的深度維持不變括丁,所以它需要運(yùn)行多久就可以運(yùn)行多久。

蹦床表達(dá)的是伶选,內(nèi)部的partial()函數(shù)使用在變量xacc上的閉包來保持迭代與迭代之間的狀態(tài)史飞。它的優(yōu)勢是循環(huán)的邏輯可以被抽出到一個可重用的trampoline(..)工具函數(shù)中尖昏,許多庫都提供這個工具的各種版本。你可以使用不同的蹦床算法在你的程序中重用trampoline(..)多次构资。

當(dāng)然抽诉,如果你真的想要深度優(yōu)化(于是可復(fù)用性不予考慮),你可以摒棄閉包狀態(tài)吐绵,并將對acc的狀態(tài)追蹤迹淌,與一個循環(huán)一起內(nèi)聯(lián)到一個函數(shù)的作用域內(nèi)。這種技術(shù)通常稱為 遞歸展開

"use strict";

function foo(x) {
    var acc = 1;
    while (x > 1) {
        acc = (x / 2) + acc;
        x = x - 1;
    }
    return acc;
}

foo( 123456 );          // 3810376848.5

算法的這種表達(dá)形式很容易閱讀己单,而且很可能是在我們探索過的各種形式中性能最好的(嚴(yán)格地說)一個唉窃。很明顯它看起來是一個勝利者,而且你可能會想知道為什么你曾嘗試其他的方式纹笼。

這些是為什么你可能不想總是手動地展開遞歸的原因:

  • 與為了復(fù)用而將彈簧(循環(huán))邏輯抽出去相比纹份,我們內(nèi)聯(lián)了它。這在僅有一個這樣的例子需要考慮時工作的很好廷痘,但只要你在程序中有五六個或更多這樣的東西時蔓涧,你將很可能想要一些可復(fù)用性來將讓事情更簡短、更易管理一些笋额。

  • 這里的例子為了展示不同的形式而被故意地搞得很簡單元暴。在現(xiàn)實中,遞歸算法有著更多的復(fù)雜性兄猩,比如相互遞歸(有多于一個的函數(shù)調(diào)用它自己)茉盏。

    你在這條路上走得越遠(yuǎn),展開 優(yōu)化就變得越復(fù)雜和越依靠手動厦滤。你很快就會失去所有可讀性的認(rèn)知價值援岩。遞歸歼狼,甚至是PTC形式的遞歸的主要優(yōu)點(diǎn)是掏导,它保留了算法的可讀性,并將性能優(yōu)化的任務(wù)交給引擎羽峰。

如果你使用PTC編寫你的算法趟咆,ES6引擎將會實施TCO來使你的代碼運(yùn)行在一個定長深度的棧中(通過重用棧幀)。你將在得到遞歸的可讀性的同時梅屉,也得到性能上的大部分好處與無限的運(yùn)行長度值纱。

元?

TCO與元編程有什么關(guān)系坯汤?

正如我們在早先的“特性測試”一節(jié)中講過的虐唠,你可以在運(yùn)行時判定一個引擎支持什么特性。這也包括TCO惰聂,雖然判定的過程相當(dāng)粗暴疆偿≡凵福考慮如下代碼:

"use strict";

try {
    (function foo(x){
        if (x < 5E5) return foo( x + 1 );
    })( 1 );

    TCO_ENABLED = true;
}
catch (err) {
    TCO_ENABLED = false;
}

在一個非TCO引擎中,遞歸循環(huán)最終將會失敗杆故,拋出一個被try..catch捕獲的異常迅箩。否則循環(huán)將由TCO輕易地完成。

討厭处铛,對吧饲趋?

但是圍繞著TCO特性進(jìn)行的元編程(或者,沒有它)如何給我們的代碼帶來好處撤蟆?簡單的答案是你可以使用這樣的特性測試來決定加載一個你的應(yīng)用程序的使用遞歸的版本奕塑,還是一個被轉(zhuǎn)換/轉(zhuǎn)譯為不需要遞歸的版本。

自我調(diào)整的代碼

但這里有另外一種看待這個問題的方式:

"use strict";

function foo(x) {
    function _foo() {
        if (x > 1) {
            acc = acc + (x / 2);
            x = x - 1;
            return _foo();
        }
    }

    var acc = 1;

    while (x > 1) {
        try {
            _foo();
        }
        catch (err) { }
    }

    return acc;
}

foo( 123456 );          // 3810376848.5

這個算法試圖盡可能多地使用遞歸來工作枫疆,但是通過作用域中的變量xacc來跟蹤這個進(jìn)程爵川。如果整個問題可以通過遞歸沒有錯誤地解決,很好息楔。如果引擎在某一點(diǎn)終止了遞歸寝贡,我們簡單地使用try..catch捕捉它,然后從我們離開的地方再試一次值依。

我認(rèn)為這是一種形式的元編程圃泡,因為你在運(yùn)行時期間探測著引擎是否能(遞歸地)完成任務(wù)的能力,并繞過了任何可能制約你的(非TCO的)引擎的限制愿险。

一眼(或者是兩眼F睦)看上去,我打賭這段代碼要比以前的版本難看許多辆亏。它運(yùn)行起來還相當(dāng)?shù)芈恍ㄔ谝粋€非TCO環(huán)境中長時間運(yùn)行的情況下)风秤。

它主要的優(yōu)勢是,除了在非TCO引擎中也能完成任意棧大小的任務(wù)外扮叨,這種對遞歸棧限制的“解法”要比前面展示的蹦床和手動展開技術(shù)靈活得多缤弦。

實質(zhì)上,這種情況下的_foo()實際上是任意遞歸任務(wù)彻磁,甚至是相互遞歸的某種替身碍沐。剩下的內(nèi)容是應(yīng)當(dāng)對任何算法都可以工作的模板代碼。

唯一的“技巧”是為了能夠在達(dá)到遞歸限制的事件發(fā)生時繼續(xù)運(yùn)行衷蜓,遞歸的狀態(tài)必須保存在遞歸函數(shù)外部的作用域變量中累提。我們是通過將xacc留在_foo()函數(shù)外面這樣做的,而不是像早先那樣將它們作為參數(shù)值傳遞給_foo()磁浇。

幾乎所有的遞歸算法都可以采用這種方法工作斋陪。這意味著它是在你的程序中,進(jìn)行最小的重寫就能利用TCO遞歸的最廣泛的可行方法。

這種方式仍然使用一個PTC无虚,意味著這段代碼將會 漸進(jìn)增強(qiáng):從在一個老版瀏覽器中使用許多次循環(huán)(遞歸批處理)來運(yùn)行鞍匾,到在一個ES6+環(huán)境中完全利用TCO遞歸。我覺得這相當(dāng)酷骑科!

復(fù)習(xí)

元編程是當(dāng)你將程序的邏輯轉(zhuǎn)向關(guān)注它自身(或者它的運(yùn)行時環(huán)境)時進(jìn)行的編程橡淑,要么為了調(diào)查它自己的結(jié)構(gòu),要么為了修改它咆爽。元編程的主要價值是擴(kuò)展語言的普通機(jī)制來提供額外的能力梁棠。

在ES6以前,JavaScript已經(jīng)有了相當(dāng)?shù)脑幊棠芰Γ荅S6使用了幾個新特性及大地提高了它的地位。

從對匿名函數(shù)的函數(shù)名推斷聪铺,到告訴你一個構(gòu)造器是如何被調(diào)用的元屬性哑芹,你可以前所未有地在程序運(yùn)行期間來調(diào)查它的結(jié)構(gòu)序芦。通用Symbols允許你覆蓋固有的行為,比如將一個對象轉(zhuǎn)換為一個基本類型值的強(qiáng)制轉(zhuǎn)換。代理可以攔截并自定義各種在對象上的底層操作,而且Reflect提供了模擬它們的工具模闲。

特性測試,即便是對尾部調(diào)用優(yōu)化這樣微妙的語法行為崭捍,將元編程的焦點(diǎn)從你的程序提升到JS引擎的能力本身尸折。通過更多地了解環(huán)境可以做什么,你的程序可以在運(yùn)行時將它們自己調(diào)整到最佳狀態(tài)殷蛇。

你應(yīng)該進(jìn)行元編程嗎实夹?我的建議是:先集中學(xué)習(xí)這門語言的核心機(jī)制是如何工作的。一旦你完全懂得了JS本身可以做什么粒梦,就是開始利用這些強(qiáng)大的元編程能力將這門語言向前推進(jìn)的時候了亮航!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市匀们,隨后出現(xiàn)的幾起案子缴淋,更是在濱河造成了極大的恐慌,老刑警劉巖昼蛀,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宴猾,死亡現(xiàn)場離奇詭異圆存,居然都是意外死亡叼旋,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門沦辙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來夫植,“玉大人,你說我怎么就攤上這事∠昝瘢” “怎么了延欠?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長沈跨。 經(jīng)常有香客問我由捎,道長,這世上最難降的妖魔是什么饿凛? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任狞玛,我火速辦了婚禮,結(jié)果婚禮上涧窒,老公的妹妹穿的比我還像新娘心肪。我一直安慰自己,他們只是感情好纠吴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布硬鞍。 她就那樣靜靜地躺著,像睡著了一般戴已。 火紅的嫁衣襯著肌膚如雪固该。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天糖儡,我揣著相機(jī)與錄音蹬音,去河邊找鬼。 笑死休玩,一個胖子當(dāng)著我的面吹牛著淆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拴疤,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼永部,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了呐矾?” 一聲冷哼從身側(cè)響起苔埋,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蜒犯,沒想到半個月后组橄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡罚随,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年玉工,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淘菩。...
    茶點(diǎn)故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡遵班,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情狭郑,我是刑警寧澤腹暖,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站翰萨,受9級特大地震影響脏答,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜亩鬼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一以蕴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辛孵,春花似錦丛肮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至冶匹,卻和暖如春习劫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嚼隘。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工诽里, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人飞蛹。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓谤狡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親卧檐。 傳聞我的和親對象是個殘疾皇子墓懂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評論 2 353

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