第七章:元編程 1

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

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

例如,如果你為了調(diào)查對象a和另一個對象b之間的關(guān)系 —— 它們是被[[Prototype]]鏈接的嗎? —— 而使用a.isPrototypeOf(b),這通常稱為自省,就是一種形式的元編程。宏(JS中還沒有) —— 代碼在編譯時修改自己 —— 是元編程的另一個明顯的例子。使用for..in循環(huán)枚舉一個對象的鍵,或者檢查一個對象是否是一個“類構(gòu)造器”的 實(shí)例,是另一些常見的元編程任務(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)生這種用困惑是因?yàn)?,在默認(rèn)情況下一個函數(shù)的詞法名稱(如果有的話)也會被設(shè)置為它的name屬性。實(shí)際上,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)造器。

這是一個元編程操作的典型例子,因?yàn)樗囊鈭D是從一個構(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()實(shí)際上被給予了這個類的詞法名稱(Parent),即便語法暗示著這個類是一個與構(gòu)造器分離的不同實(shí)體。

警告: 與所有的元編程技術(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í)施什么操作。對于對象,最常見的兩個自省技術(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ù)上的方法,它接收一個實(shí)例對象值并讓你通過放回truefalse來決定這個值是否應(yīng)當(dāng)被認(rèn)為是一個實(shí)例。

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

Symbol.species

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

最常見的例子是,在子類化Array并且想要定義slice(..)之類被繼承的方法應(yīng)當(dāng)使用哪一個構(gòu)造器時。默認(rèn)地,在一個Array的子類實(shí)例上調(diào)用的slice(..)將產(chǎn)生這個子類的實(shí)例,坦白地說這正是你經(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)值,但也像展示的那樣,這種行為很容易模擬。

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

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)僅用于確實(shí)必要或有好處的情況下。

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àn)閼?yīng)當(dāng)避免with,所以這個@@unscopablessymbol也是無意義的。

代理

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

一個代理是一種由你創(chuàng)建的特殊的對象,它“包”著另一個普通的對象 —— 或者說擋在這個普通對象的前面。你可以在代理對象上注冊特殊的處理器(也叫 機(jī)關(guān)(traps)),當(dāng)對這個代理實(shí)施各種操作時被調(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ù)實(shí)施時進(jìn)行攔截,而每個Reflect工具將各自的元編程任務(wù)在一個對象上實(shí)施。每個代理處理器都有一個自動調(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)是顯而易見的。我們可以全面地?cái)r截(而因此覆蓋)對象的行為,這意味著我們可以用一些非常強(qiáng)大的方式將對象行為擴(kuò)展至JS核心之外。我們將看幾個模式的例子來探索這些可能性。

代理前置,代理后置

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

你可能這么做是因?yàn)槟阆M麑ο髠鬟f到某個你不能完全“信任”的地方去,如此你需要在它的訪問權(quán)上強(qiáng)制實(shí)施一些特殊的規(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);
                }
            }
            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)槲覀兪紫龋ㄖ饕?、完全地)與代理進(jìn)行互動。

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

另一種方式是,我們可以完全反轉(zhuǎn)這個模式,讓目標(biāo)與代理交互而不是讓代理與目標(biāo)交互。這樣,代碼其實(shí)只與主對象交互。達(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(..)。

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

"No Such Property/Method"

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

我們可以使用一個代理來達(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!

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

現(xiàn)在,讓我們考慮一下反過來的 代理后置 設(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)簡單。與攔截[[Get]][[Set]]操作并僅在目標(biāo)屬性存在時轉(zhuǎn)送它們不同,我們依賴于這樣一個事實(shí):不管[[Get]]還是[[Set]]到達(dá)了我們的pobj后備對象,這個動作已經(jīng)遍歷了整個[[Prototype]]鏈并且沒有找到匹配的屬性。在這時我們可以自由地、無條件地拋出錯誤。很酷,對吧?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容