第七章:元編程 1

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

元編程是針對(duì)程序本身的行為進(jìn)行操作的編程。換句話說(shuō)傅是,它是為你程序的編程而進(jìn)行的編程。是的挣输,很拗口空凸,對(duì)吧?

例如鲫咽,如果你為了調(diào)查對(duì)象a和另一個(gè)對(duì)象b之間的關(guān)系 —— 它們是被[[Prototype]]鏈接的嗎? —— 而使用a.isPrototypeOf(b)谷异,這通常稱為自省分尸,就是一種形式的元編程。宏(JS中還沒有) —— 代碼在編譯時(shí)修改自己 —— 是元編程的另一個(gè)明顯的例子歹嘹。使用for..in循環(huán)枚舉一個(gè)對(duì)象的鍵箩绍,或者檢查一個(gè)對(duì)象是否是一個(gè)“類構(gòu)造器”的 實(shí)例,是另一些常見的元編程任務(wù)尺上。

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

元編程的目標(biāo)是利用語(yǔ)言自身的內(nèi)在能力使你其他部分的代碼更具描述性卑吭,表現(xiàn)力芽淡,和/或靈活性。由于元編程的 的性質(zhì)豆赏,要給它一個(gè)更精確的定義有些困難挣菲。理解元編程的最佳方法是通過(guò)代碼來(lái)觀察它。

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

函數(shù)名

有一些情況,你的代碼想要檢視自己并詢問(wèn)某個(gè)函數(shù)的名稱是什么抚岗。如果你詢問(wèn)一個(gè)函數(shù)的名稱或杠,答案會(huì)有些令人詫異地模糊⌒担考慮如下代碼:

function daz() {
    // ..
}

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

在這前一個(gè)代碼段中向抢,“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ù)有一個(gè)叫做name的屬性 —— 還是指它詞法綁定的名稱啸驯,比如在function bar() { .. }中的bar客扎。

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

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

name屬性是你為了元編程而使用的,所以它才是我們?cè)谶@里的討論中所關(guān)注的罚斗。

產(chǎn)生這種用困惑是因?yàn)獒阌悖谀J(rèn)情況下一個(gè)函數(shù)的詞法名稱(如果有的話)也會(huì)被設(shè)置為它的name屬性。實(shí)際上,ES5(和以前的)語(yǔ)言規(guī)范中并沒有官方要求這種行為袱吆。name屬性的設(shè)置是一種非標(biāo)準(zhǔn)厌衙,但依然相當(dāng)可靠的行為。在ES6中绞绒,它已經(jīng)被標(biāo)準(zhǔn)化婶希。

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

推斷

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

現(xiàn)在在ES6中撤蟆,有一個(gè)推斷規(guī)則可以判定一個(gè)合理的name屬性值來(lái)賦予一個(gè)函數(shù),即使它沒有詞法名稱可用堂污。

考慮如下代碼:

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

abc.name;               // "abc"

如果我們給了這個(gè)函數(shù)一個(gè)詞法名稱家肯,比如abc = function def() { .. },那么name屬性將理所當(dāng)然地是"def"盟猖。但是由于缺少詞法名稱讨衣,直觀上名稱"abc"看起來(lái)很合適。

這里是在ES6中將會(huì)(或不會(huì))進(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(..)來(lái)手動(dòng)改變它娘汞。

元屬性

在第三章的“new.target”一節(jié)中歹茶,我們引入了一個(gè)ES6的新概念:元屬性。正如這個(gè)名稱所暗示的你弦,元屬性意在以一種屬性訪問(wèn)的形式提供特殊的元信息惊豺,而這在以前是不可能的。

new.target的情況下禽作,關(guān)鍵字new作為一個(gè)屬性訪問(wèn)的上下文環(huán)境尸昧。顯然new本身不是一個(gè)對(duì)象,這使得這種能力很特殊旷偿。然而烹俗,當(dāng)new.target被用于一個(gè)構(gòu)造器調(diào)用(一個(gè)使用new調(diào)用的函數(shù)/方法)內(nèi)部時(shí),new變成了一個(gè)虛擬上下文環(huán)境萍程,如此new.target就可以指代這個(gè)new調(diào)用的目標(biāo)構(gòu)造器幢妄。

這是一個(gè)元編程操作的典型例子,因?yàn)樗囊鈭D是從一個(gè)構(gòu)造器調(diào)用內(nèi)部判定原來(lái)的new的目標(biāo)是什么茫负,這一般是為了自蚀沤健(檢查類型/結(jié)構(gòu))或者靜態(tài)屬性訪問(wèn)。

舉例來(lái)說(shuō)朽褪,你可能想根據(jù)一個(gè)構(gòu)造器是被直接調(diào)用置吓,還是通過(guò)一個(gè)子類進(jìn)行調(diào)用无虚,來(lái)使它有不同的行為:

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

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

警告: 與所有的元編程技術(shù)一樣,要小心不要?jiǎng)?chuàng)建太過(guò)聰明的代碼戴质,而使未來(lái)的你或其他維護(hù)你代碼的人很難理解度宦。小心使用這些技巧。

通用 Symbol

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

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

我們將簡(jiǎn)要介紹每一個(gè)symbol并討論它們的目的戚哎。

Symbol.iterator

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

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

然而疏唾,我們可以通過(guò)設(shè)置Symbol.iterator屬性來(lái)為任意對(duì)象定義我們自己的迭代器邏輯粒梦,即便它是覆蓋默認(rèn)迭代器的。這里的元編程觀點(diǎn)是荸实,我們?cè)诙xJS的其他部分(明確地說(shuō)匀们,是操作符和循環(huán)結(jié)構(gòu))在處理我們所定義的對(duì)象值時(shí)所使用的行為。

考慮如下代碼:

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

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

// 定義一個(gè)僅在奇數(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ù)之一准给,就是在一個(gè)值上進(jìn)行自省來(lái)找出它是什么 種類 的泄朴,者經(jīng)常用來(lái)決定它們上面適于實(shí)施什么操作。對(duì)于對(duì)象露氮,最常見的兩個(gè)自省技術(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指定一個(gè)用于[object ___]字符串化的字符串值畔规。

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

注意: 要在一個(gè)函數(shù)上設(shè)置@@hasInstance,你必須使用Object.defineProperty(..)三妈,因?yàn)樵?code>Function.prototype上默認(rèn)的那一個(gè)是writable: false畜埋。更多信息參見本系列的 this與對(duì)象原型

Symbol.species

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

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

但是,你可以通過(guò)覆蓋一個(gè)類的默認(rèn)@@species定義來(lái)進(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)值,但也像展示的那樣戴已,這種行為很容易模擬。

如果你需要定義生成新實(shí)例的方法锅减,使用new this.constructor[Symbol.species](..)的元編程模式糖儡,而不要用手寫的new this.constructor(..)或者new XYZ(..)。如此衍生的類就能夠自定義Symbol.species來(lái)控制哪一個(gè)構(gòu)造器來(lái)制造這些實(shí)例怔匣。

Symbol.toPrimitive

在本系列的 類型與文法 一書中握联,我們討論了ToPrimitive抽象強(qiáng)制轉(zhuǎn)換操作,它在對(duì)象為了某些操作(例如==比較或者+加法)而必須被強(qiáng)制轉(zhuǎn)換為一個(gè)基本類型值時(shí)被使用每瞒。在ES6以前金闽,沒有辦法控制這個(gè)行為。

在ES6中剿骨,在任意對(duì)象值上作為屬性的@@toPrimitivesymbol都可以通過(guò)指定一個(gè)方法來(lái)自定義這個(gè)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的操作期望何種類型,而被提供一個(gè)值為"string"浓利,"number"挤庇,或"default"(這應(yīng)當(dāng)被解釋為"number")的 提示(hint)。在前一個(gè)代碼段中贷掖,+加法操作沒有提示("default"將被傳遞)嫡秕。一個(gè)*乘法操作將提示"number",而一個(gè)String(arr)將提示"string"苹威。

警告: ==操作符將在一個(gè)對(duì)象上不使用任何提來(lái)示調(diào)用ToPrimitive操作 —— 如果存在@@toPrimitive方法的話昆咽,將使用"default"被調(diào)用 —— 如果另一個(gè)被比較的值不是一個(gè)對(duì)象。但是,如果兩個(gè)被比較的值都是對(duì)象掷酗,==的行為與===是完全相同的调违,也就是引用本身將被直接比較。這種情況下汇在,@@toPrimitive根本不會(huì)被調(diào)用翰萨。關(guān)于強(qiáng)制轉(zhuǎn)換和抽象操作的更多信息,參見本系列的 類型與文法糕殉。

正則表達(dá)式 Symbols

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

  • @@match:一個(gè)正則表達(dá)式的Symbol.match值是使用被給定的正則表達(dá)式來(lái)匹配一個(gè)字符串值的全部或部分的方法阿蝶。如果你為String.prototype.match(..)傳遞一個(gè)正則表達(dá)式做范例匹配雳锋,它就會(huì)被使用。

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

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

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

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

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

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

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

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

    分割的默認(rèn)算法寫在ES6語(yǔ)言規(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)僅用于確實(shí)必要或有好處的情況下盅粪。

Symbol.isConcatSpreadable

@@isConcatSpreadablesymbol可以作為一個(gè)布爾屬性(Symbol.isConcatSpreadable)在任意對(duì)象上(比如一個(gè)數(shù)組或其他的可迭代對(duì)象)定義,來(lái)指示當(dāng)它被傳遞給一個(gè)數(shù)組concat(..)時(shí)是否應(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可以作為一個(gè)對(duì)象屬性(Symbol.unscopables)在任意對(duì)象上定義票顾,來(lái)指示在一個(gè)with語(yǔ)句中哪一個(gè)屬性可以和不可以作為此法變量被暴露础浮。

考慮如下代碼:

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
}

一個(gè)在@@unscopables對(duì)象中的true指示這個(gè)屬性應(yīng)當(dāng)是 非作用域(unscopable) 的,因此會(huì)從此法作用域變量中被過(guò)濾掉奠骄。false意味著它可以被包含在此法作用域變量中豆同。

警告: with語(yǔ)句在strict模式下是完全禁用的,而且因此應(yīng)當(dāng)被認(rèn)為是在語(yǔ)言中被廢棄的含鳞。不要使用它影锈。更多信息參見本系列的 作用域與閉包。因?yàn)閼?yīng)當(dāng)避免with蝉绷,所以這個(gè)@@unscopablessymbol也是無(wú)意義的鸭廷。

代理

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

一個(gè)代理是一種由你創(chuàng)建的特殊的對(duì)象熔吗,它“包”著另一個(gè)普通的對(duì)象 —— 或者說(shuō)擋在這個(gè)普通對(duì)象的前面辆床。你可以在代理對(duì)象上注冊(cè)特殊的處理器(也叫 機(jī)關(guān)(traps)),當(dāng)對(duì)這個(gè)代理實(shí)施各種操作時(shí)被調(diào)用桅狠。這些處理器除了將操作 傳送 到原本的目標(biāo)/被包裝的對(duì)象上之外讼载,還有機(jī)會(huì)運(yùn)行額外的邏輯。

一個(gè)這樣的 機(jī)關(guān) 處理器的例子是中跌,你可以在一個(gè)代理上定義一個(gè)攔截[[Get]]操作的get —— 它在當(dāng)你試圖訪問(wèn)一個(gè)對(duì)象上的屬性時(shí)運(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

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

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

這些映射是故意對(duì)稱的咪鲜。每個(gè)代理處理器在各自的元編程任務(wù)實(shí)施時(shí)進(jìn)行攔截狐赡,而每個(gè)Reflect工具將各自的元編程任務(wù)在一個(gè)對(duì)象上實(shí)施。每個(gè)代理處理器都有一個(gè)自動(dòng)調(diào)用相應(yīng)Reflect工具的默認(rèn)定義疟丙。幾乎可以肯定你將總是一前一后地使用ProxyReflect颖侄。

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

  • get(..):通過(guò)[[Get]]享郊,在代理上訪問(wèn)一個(gè)屬性(Reflect.get(..)览祖,.屬性操作符或[ .. ]屬性操作符)
  • set(..):通過(guò)[[Set]],在代理對(duì)象上設(shè)置一個(gè)屬性(Reflect.set(..)炊琉,=賦值操作符展蒂,或者解構(gòu)賦值 —— 如果目標(biāo)是一個(gè)對(duì)象屬性的話)
  • deleteProperty(..):通過(guò)[[Delete]]又活,在代理對(duì)象上刪除一個(gè)屬性 (Reflect.deleteProperty(..)delete)
  • apply(..)(如果 目標(biāo) 是一個(gè)函數(shù)):通過(guò)[[Call]],代理作為一個(gè)普通函數(shù)/方法被調(diào)用(Reflect.apply(..)锰悼,call(..)柳骄,apply(..),或者(..)調(diào)用操作符)
  • construct(..)(如果 目標(biāo) 是一個(gè)構(gòu)造函數(shù)):通過(guò)[[Construct]]代理作為一個(gè)構(gòu)造器函數(shù)被調(diào)用(Reflect.construct(..)new
  • getOwnPropertyDescriptor(..):通過(guò)[[GetOwnProperty]]箕般,從代理取得一個(gè)屬性的描述符(Object.getOwnPropertyDescriptor(..)Reflect.getOwnPropertyDescriptor(..)
  • defineProperty(..):通過(guò)[[DefineOwnProperty]]耐薯,在代理上設(shè)置一個(gè)屬性描述符(Object.defineProperty(..)Reflect.defineProperty(..)
  • getPrototypeOf(..):通過(guò)[[GetPrototypeOf]],取得代理的[[Prototype]]Object.getPrototypeOf(..)丝里,Reflect.getPrototypeOf(..)曲初,__proto__, Object#isPrototypeOf(..),或instanceof
  • setPrototypeOf(..):通過(guò)[[SetPrototypeOf]]丙者,設(shè)置代理的[[Prototype]]Object.setPrototypeOf(..)复斥,Reflect.setPrototypeOf(..),或__proto__
  • preventExtensions(..):通過(guò)[[PreventExtensions]]使代理成為不可擴(kuò)展的(Object.preventExtensions(..)Reflect.preventExtensions(..)
  • isExtensible(..):通過(guò)[[IsExtensible]]械媒,檢測(cè)代理的可擴(kuò)展性(Object.isExtensible(..)Reflect.isExtensible(..)
  • ownKeys(..):通過(guò)[[OwnPropertyKeys]]目锭,取得一組代理的直屬屬性和/或直屬symbol屬性(Object.keys(..)Object.getOwnPropertyNames(..)纷捞,Object.getOwnSymbolProperties(..)痢虹,Reflect.ownKeys(..),或JSON.stringify(..)
  • enumerate(..):通過(guò)[[Enumerate]]主儡,為代理的可枚舉直屬屬性及“繼承”屬性請(qǐng)求一個(gè)迭代器(Reflect.enumerate(..)for..in
  • has(..):通過(guò)[[HasProperty]]奖唯,檢測(cè)代理是否擁有一個(gè)直屬屬性或“繼承”屬性(Reflect.has(..)Object#hasOwnProperty(..)糜值,或"prop" in obj

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

關(guān)于將會(huì)觸發(fā)各種機(jī)關(guān)的動(dòng)作寂汇,除了在前面列表中記載的以外病往,一些機(jī)關(guān)還會(huì)由另一個(gè)機(jī)關(guān)的默認(rèn)動(dòng)作間接地觸發(fā)。舉例來(lái)說(shuō):

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è)置一個(gè)屬性值時(shí)(不管是新添加還是更新)骄瓣,getOwnPropertyDescriptor(..)defineProperty(..)處理器被默認(rèn)的set(..)處理器觸發(fā)停巷。如果你還定義了你自己的set(..)處理器,你或許對(duì)context(不是targeti爬浮)進(jìn)行了將會(huì)觸發(fā)這些代理機(jī)關(guān)的相應(yīng)調(diào)用畔勤。

代理的限制

這些元編程處理器攔截了你可以對(duì)一個(gè)對(duì)象進(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

也許在未來(lái)缸榛,更多這些語(yǔ)言中的底層基礎(chǔ)操作都將是可攔截的检访,那將給我們更多力量來(lái)從JavaScript自身擴(kuò)展它。

警告: 對(duì)于代理處理器的使用來(lái)說(shuō)存在某些 不變量 —— 它們的行為不能被覆蓋仔掸。例如脆贵,isExtensible(..)處理器的結(jié)果總是被強(qiáng)制轉(zhuǎn)換為一個(gè)boolean。這些不變量限制了一些你可以使用代理來(lái)自定義行為的能力起暮,但是它們這樣做只是為了防止你創(chuàng)建奇怪和不尋常(或不合邏輯)的行為卖氨。這些不變量的條件十分復(fù)雜,所以我們就不再這里全面闡述了负懦,但是這篇博文(http://www.2ality.com/2014/12/es6-proxies.html#invariants)很好地講解了它們筒捺。

可撤銷的代理

一個(gè)一般的代理總是包裝著目標(biāo)對(duì)象,而且在創(chuàng)建之后就不能修改了 —— 只要保持著一個(gè)指向這個(gè)代理的引用纸厉,代理的機(jī)制就將維持下去系吭。但是,可能會(huì)有一些情況你想要?jiǎng)?chuàng)建一個(gè)這樣的代理:在你想要停止它作為代理時(shí)可以被停用颗品。解決方案就是創(chuàng)建一個(gè) 可撤銷代理

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

一個(gè)可撤銷代理是由Proxy.revocable(..)創(chuàng)建的肯尺,它是一個(gè)普通的函數(shù),不是一個(gè)像Proxy(..)那樣的構(gòu)造器躯枢。此外则吟,它接收同樣的兩個(gè)參數(shù)值:目標(biāo)處理器

new Proxy(..)不同的是锄蹂,Proxy.revocable(..)的返回值不是代理本身氓仲。取而代之的是,它返回一個(gè)帶有 proxyrevoke 兩個(gè)屬性的對(duì)象 —— 我們使用了對(duì)象解構(gòu)(參見第二章的“解構(gòu)”)來(lái)將這些屬性分別賦值給變量pobjprevoke得糜。

一旦可撤銷代理被撤銷敬扛,任何訪問(wèn)它的企圖(觸發(fā)它的任何機(jī)關(guān))都將拋出TypeError

一個(gè)使用可撤銷代理的例子可能是朝抖,將一個(gè)代理交給另一個(gè)存在于你應(yīng)用中啥箭、并管理你模型中的數(shù)據(jù)的團(tuán)體,而不是給它們一個(gè)指向正式模型對(duì)象本身的引用槽棍。如果你的模型對(duì)象改變了或者被替換掉了捉蚤,你希望廢除這個(gè)你交出去的代理抬驴,以便于其他的團(tuán)體能夠(通過(guò)錯(cuò)誤A镀摺)知道要請(qǐng)求一個(gè)更新過(guò)的模型引用。

使用代理

這些代理處理器帶來(lái)的元編程的好處應(yīng)當(dāng)是顯而易見的布持。我們可以全面地?cái)r截(而因此覆蓋)對(duì)象的行為豌拙,這意味著我們可以用一些非常強(qiáng)大的方式將對(duì)象行為擴(kuò)展至JS核心之外。我們將看幾個(gè)模式的例子來(lái)探索這些可能性题暖。

代理前置按傅,代理后置

正如我們?cè)缦忍岬竭^(guò)的捉超,你通常將一個(gè)代理考慮為一個(gè)目標(biāo)對(duì)象的“包裝”。在這種意義上唯绍,代理就變成了代碼接口所針對(duì)的主要對(duì)象拼岳,而實(shí)際的目標(biāo)對(duì)象則保持被隱藏/被保護(hù)的狀態(tài)。

你可能這么做是因?yàn)槟阆M麑?duì)象傳遞到某個(gè)你不能完全“信任”的地方去况芒,如此你需要在它的訪問(wèn)權(quán)上強(qiáng)制實(shí)施一些特殊的規(guī)則惜纸,而不是傳遞這個(gè)對(duì)象本身。

考慮如下代碼:

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

            // 讓其余的東西通過(guò)
            return target[key];
        },
        set(target,key,val) {
            // 僅設(shè)置唯一的小寫字符串
            if (typeof val == "string") {
                val = val.toLowerCase();
                if (target.indexOf( val ) == -1) {
                    target.push(val);
                }
            }
            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ì)耐版,因?yàn)槲覀兪紫龋ㄖ饕⑼耆兀┡c代理進(jìn)行互動(dòng)压汪。

我們?cè)谂cmessages_proxy的互動(dòng)上強(qiáng)制實(shí)施了一些特殊規(guī)則粪牲,這些規(guī)則不會(huì)強(qiáng)制實(shí)施在messages本身上。我們僅在值是一個(gè)不重復(fù)的字符串時(shí)才將它添加為元素止剖;我們還將這個(gè)值變?yōu)樾懴傺簟.?dāng)從messages_proxy取得值時(shí),我們過(guò)濾掉字符串中所有的標(biāo)點(diǎn)符號(hào)穿香。

另一種方式是舌狗,我們可以完全反轉(zhuǎn)這個(gè)模式,讓目標(biāo)與代理交互而不是讓代理與目標(biāo)交互扔水。這樣痛侍,代碼其實(shí)只與主對(duì)象交互。達(dá)成這種后備方案的最簡(jiǎn)單的方法是魔市,讓代理對(duì)象存在于主對(duì)象的[[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(..)時(shí)待德,它在greeter上被找到并直接使用君丁。但當(dāng)我們?cè)噲D訪問(wèn)everyone()這樣的方法時(shí),這個(gè)函數(shù)并不存在于greeter将宪。

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

我稱這種模式為 代理后置,因?yàn)榇韮H被用作最后一道防線丑勤。

"No Such Property/Method"

一個(gè)關(guān)于JS的常見的抱怨是华嘹,在你試著訪問(wèn)或設(shè)置一個(gè)對(duì)象上還不存在的屬性時(shí),默認(rèn)情況下對(duì)象不是非常具有防御性法竞。你可能希望為一個(gè)對(duì)象預(yù)定義所有這些屬性/方法耙厚,而且在后續(xù)使用不存在的屬性名時(shí)拋出一個(gè)錯(cuò)誤强挫。

我們可以使用一個(gè)代理來(lái)達(dá)成這種想法,既可以使用 代理前置 也可以 代理后置 設(shè)計(jì)薛躬。我們將兩者都考慮一下俯渤。

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!

對(duì)于get(..)set(..)兩者,我們僅在目標(biāo)對(duì)象的屬性已經(jīng)存在時(shí)才轉(zhuǎn)送操作型宝;否則拋出錯(cuò)誤稠诲。代理對(duì)象應(yīng)當(dāng)是進(jìn)行交互的主對(duì)象,因?yàn)樗鼣r截這些操作來(lái)提供保護(hù)诡曙。

現(xiàn)在臀叙,讓我們考慮一下反過(guò)來(lái)的 代理后置 設(shè)計(jì):

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è)計(jì)相當(dāng)簡(jiǎn)單价卤。與攔截[[Get]][[Set]]操作并僅在目標(biāo)屬性存在時(shí)轉(zhuǎn)送它們不同劝萤,我們依賴于這樣一個(gè)事實(shí):不管[[Get]]還是[[Set]]到達(dá)了我們的pobj后備對(duì)象,這個(gè)動(dòng)作已經(jīng)遍歷了整個(gè)[[Prototype]]鏈并且沒有找到匹配的屬性慎璧。在這時(shí)我們可以自由地床嫌、無(wú)條件地拋出錯(cuò)誤。很酷胸私,對(duì)吧厌处?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市岁疼,隨后出現(xiàn)的幾起案子阔涉,更是在濱河造成了極大的恐慌,老刑警劉巖捷绒,帶你破解...
    沈念sama閱讀 221,576評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瑰排,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡暖侨,警方通過(guò)查閱死者的電腦和手機(jī)椭住,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)字逗,“玉大人京郑,你說(shuō)我怎么就攤上這事『簦” “怎么了些举?”我有些...
    開封第一講書人閱讀 168,017評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)挖息。 經(jīng)常有香客問(wèn)我金拒,道長(zhǎng)兽肤,這世上最難降的妖魔是什么套腹? 我笑而不...
    開封第一講書人閱讀 59,626評(píng)論 1 296
  • 正文 為了忘掉前任绪抛,我火速辦了婚禮,結(jié)果婚禮上电禀,老公的妹妹穿的比我還像新娘幢码。我一直安慰自己,他們只是感情好尖飞,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評(píng)論 6 397
  • 文/花漫 我一把揭開白布症副。 她就那樣靜靜地躺著,像睡著了一般政基。 火紅的嫁衣襯著肌膚如雪贞铣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,255評(píng)論 1 308
  • 那天沮明,我揣著相機(jī)與錄音辕坝,去河邊找鬼。 笑死荐健,一個(gè)胖子當(dāng)著我的面吹牛酱畅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播江场,決...
    沈念sama閱讀 40,825評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼纺酸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了址否?” 一聲冷哼從身側(cè)響起餐蔬,我...
    開封第一講書人閱讀 39,729評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎佑附,沒想到半個(gè)月后用含,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,271評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡帮匾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評(píng)論 3 340
  • 正文 我和宋清朗相戀三年啄骇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瘟斜。...
    茶點(diǎn)故事閱讀 40,498評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡缸夹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出螺句,到底是詐尸還是另有隱情虽惭,我是刑警寧澤,帶...
    沈念sama閱讀 36,183評(píng)論 5 350
  • 正文 年R本政府宣布蛇尚,位于F島的核電站芽唇,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜匆笤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評(píng)論 3 333
  • 文/蒙蒙 一研侣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧炮捧,春花似錦庶诡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至书蚪,卻和暖如春喇澡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背殊校。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工撩幽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人箩艺。 一個(gè)月前我還...
    沈念sama閱讀 48,906評(píng)論 3 376
  • 正文 我出身青樓窜醉,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親艺谆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子榨惰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評(píng)論 2 359

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

  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券静汤,享受所有官網(wǎng)優(yōu)惠琅催,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 6,554評(píng)論 3 22
  • 特別說(shuō)明,為便于查閱虫给,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS...
    殺破狼real閱讀 476評(píng)論 0 1
  • ECMAScript發(fā)展歷史 (1)ECMA-262 第1版:去除了對(duì)針對(duì)瀏覽器的特性抹估,支持Unicode標(biāo)準(zhǔn)(多...
    congnie116閱讀 1,881評(píng)論 0 2
  • 一缠黍、ES6簡(jiǎn)介 ? 歷時(shí)將近6年的時(shí)間來(lái)制定的新 ECMAScript 標(biāo)準(zhǔn) ECMAScript 6(亦稱 ...
    一歲一枯榮_閱讀 6,082評(píng)論 8 25
  • TITLE: 編程語(yǔ)言亂燉 碼農(nóng)最大的煩惱——編程語(yǔ)言太多。不是我不學(xué)習(xí)药蜻,這世界變化快瓷式! 有時(shí)候還是蠻懷念十幾、二...
    碼園老農(nóng)閱讀 5,331評(píng)論 2 35