特別說(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
在第二和第三章中裸诽,我們介紹并使用了@@iterator
symbol,它被自動(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.toStringTag
和 Symbol.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 ___]
字符串化的字符串值畔规。
@@hasInstance
symbol是一個(gè)在構(gòu)造器函數(shù)上的方法局扶,它接收一個(gè)實(shí)例對(duì)象值并讓你通過(guò)放回true
或false
來(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
在第三章的“類”中畴蒲,我們介紹了@@species
symbol悠鞍,它控制一個(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ì)象值上作為屬性的@@toPrimitive
symbol都可以通過(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
@@isConcatSpreadable
symbol可以作為一個(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
@@unscopables
symbol可以作為一個(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è)@@unscopables
symbol也是無(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ì)講解Reflect
API灼伤,但要注意的是每個(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)定義疟丙。幾乎可以肯定你將總是一前一后地使用Proxy
和Reflect
颖侄。
這里的列表是你可以在一個(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
(不是target
i爬浮)進(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è)帶有 proxy 和 revoke 兩個(gè)屬性的對(duì)象 —— 我們使用了對(duì)象解構(gòu)(參見第二章的“解構(gòu)”)來(lái)將這些屬性分別賦值給變量pobj
和prevoke
得糜。
一旦可撤銷代理被撤銷敬扛,任何訪問(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ì)吧厌处?