感謝社區(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
在第二和第三章中,我們介紹并使用了@@iterator
symbol伏伐,它被自動地用于...
擴(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.toStringTag
和 Symbol.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 ___]
字符串化的字符串值寸齐。
@@hasInstance
symbol是一個在構(gòu)造器函數(shù)上的方法欲诺,它接收一個實例對象值并讓你通過放回true
或false
來決定這個值是否應(yīng)當(dāng)被認(rèn)為是一個實例。
注意: 要在一個函數(shù)上設(shè)置@@hasInstance
渺鹦,你必須使用Object.defineProperty(..)
扰法,因為在Function.prototype
上默認(rèn)的那一個是writable: false
。更多信息參見本系列的 this與對象原型毅厚。
Symbol.species
在第三章的“類”中塞颁,我們介紹了@@species
symbol,它控制一個類內(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中茉唉,在任意對象值上作為屬性的@@toPrimitive
symbol都可以通過指定一個方法來自定義這個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
@@isConcatSpreadable
symbol可以作為一個布爾屬性(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
@@unscopables
symbol可以作為一個對象屬性(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
丘喻,所以這個@@unscopables
symbol也是無意義的脯宿。
代理
在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ì)講解Reflect
API,但要注意的是每個可用的代理機(jī)關(guān)都有一個相應(yīng)的同名Reflect
函數(shù)箭阶。
這些映射是故意對稱的虚茶。每個代理處理器在各自的元編程任務(wù)實施時進(jìn)行攔截,而每個Reflect
工具將各自的元編程任務(wù)在一個對象上實施仇参。每個代理處理器都有一個自動調(diào)用相應(yīng)Reflect
工具的默認(rèn)定義嘹叫。幾乎可以肯定你將總是一前一后地使用Proxy
和Reflect
。
這里的列表是你可以在一個代理上為一個 目標(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(..)
的返回值不是代理本身实牡。取而代之的是陌僵,它返回一個帶有 proxy 和 revoke 兩個屬性的對象 —— 我們使用了對象解構(gòu)(參見第二章的“解構(gòu)”)來將這些屬性分別賦值給變量pobj
和prevoke
。
一旦可撤銷代理被撤銷创坞,任何訪問它的企圖(觸發(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
是否存在于代理上床佳。如果每個有,操作就被手動地交給存儲在target
的Symbol.for("[[Prototype]]")
位置中的對象引用蔓挖。
這種模式的一個重要優(yōu)點(diǎn)是,在obj1
和obj2
之間建立循環(huán)關(guān)系幾乎沒有入侵它們的定義馆衔。雖然前面的代碼段為了簡短而將所有的步驟交織在一起瘟判,但是如果你仔細(xì)觀察怨绣,代理處理器的邏輯完全是范用的(不具體地知道obj1
或obj2
)。所以拷获,這段邏輯可以抽出到一個簡單的將它們連在一起的幫助函數(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è)置為多重委托到obj1
和obj2
拇砰。在obj2.baz()
中梅忌,this.foo()
調(diào)用最終成為從obj1
中抽出foo()
(先到先得,雖然還有一個在obj2
上的foo()
)除破。如果我們將連接重新排列為obj2, obj1
牧氮,那么obj2.foo()
將被找到并使用。
同理瑰枫,this.bar()
調(diào)用沒有在obj1
上找到bar()
踱葛,所以它退而檢查obj2
,這里找到了一個匹配躁垛。
obj1
和obj2
代表obj3
的兩個平行的[[Prototype]]
鏈剖毯。obj1
和/或obj2
自身可以擁有委托至其他對象的普通[[Prototype]]
,或者自身也可以是多重委托的代理(就像obj3
一樣)教馆。
正如先前的循環(huán)[[Prototype]]
的例子一樣逊谋,obj1
,obj2
和obj3
的定義幾乎完全與處理多重委托的范用代理邏輯相分離土铺。定義一個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(..)
有保證()。
這個順序是:
- 首先汽绢,以數(shù)字上升的順序吗跋,枚舉所有數(shù)字索引的直屬屬性。
- 然后宁昭,以創(chuàng)建順序枚舉剩下的直屬字符串屬性名跌宛。
- 最后,以創(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_ENABLED
是true
還是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 + 1
在bar(..)
調(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)中:
- 首先诫龙,我們將
return _foo ..
這一行包裝進(jìn)函數(shù)表達(dá)式return partial() {..
。 - 然后我們將
_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ù)使用在變量x
和acc
上的閉包來保持迭代與迭代之間的狀態(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
這個算法試圖盡可能多地使用遞歸來工作枫疆,但是通過作用域中的變量x
和acc
來跟蹤這個進(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ù)外部的作用域變量中累提。我們是通過將x
和acc
留在_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)的時候了亮航!