你不懂JS:ES6與未來之元編程(中)
代理
在ES6中被加入的最明顯的元編程特性之一就是proxy特性。
一個代理是一種由你創建的特殊的對象,它「包」著另一個普通的對象 —— 或者說擋在這個普通對象的前面。你可以在代理對象上註冊特殊的處理器(也叫 機關(traps)),當對這個代理實施各種操作時被調用。這些處理器除了將操作 傳送 到原本的目標/被包裝的對象上之外,還有機會運行額外的邏輯。
一個這樣的 機關 處理器的例子是,你可以在一個代理上定義一個攔截[[Get]]操作的get —— 它在當你試圖訪問一個對象上的屬性時運行。考慮如下代碼:
varobj={a:1},
handlers={
get(target,key,context){
// 注意:target === obj,
// context === pobj
console.log("accessing: ",key);
returnReflect.get(
target,key,context
);
}
},
pobj=newProxy(obj,handlers);
obj.a;
// 1
pobj.a;
// accessing: a
// 1
我們將一個get(..)處理器作為 處理器 對象的命名方法聲明(Proxy(..)的第二個參數值),它接收一個指向 目標 對象的引用(obj),屬性的 鍵 名稱("a"),和self/接受者/代理本身(pobj)。
在追蹤語句console.log(..)之後,我們通過Reflect.get(..)將操作「轉送」到obj。我們將在下一節詳細講解ReflectAPI,但要注意的是每個可用的代理機關都有一個相應的同名Reflect函數。
這些映射是故意對稱的。每個代理處理器在各自的元編程任務實施時進行攔截,而每個Reflect工具將各自的元編程任務在一個對象上實施。每個代理處理器都有一個自動調用相應Reflect工具的默認定義。幾乎可以肯定你將總是一前一後地使用Proxy和Reflect。
這裡的列表是你可以在一個代理上為一個 目標 對象/函數定義的處理器,以及它們如何/何時被觸發:
get(..):通過[[Get]],在代理上訪問一個屬性(Reflect.get(..),.屬性操作符或[ .. ]屬性操作符)
set(..):通過[[Set]],在代理對象上設置一個屬性(Reflect.set(..),=賦值操作符,或者解構賦值 —— 如果目標是一個對象屬性的話)
deleteProperty(..):通過[[Delete]],在代理對象上刪除一個屬性 (Reflect.deleteProperty(..)或delete)
apply(..)(如果 目標 是一個函數):通過[[Call]],代理作為一個普通函數/方法被調用(Reflect.apply(..),call(..),apply(..),或者(..)調用操作符)
construct(..)(如果 目標 是一個構造函數):通過[[Construct]]代理作為一個構造器函數被調用(Reflect.construct(..)或new)
getOwnPropertyDescriptor(..):通過[[GetOwnProperty]],從代理取得一個屬性的描述符(Object.getOwnPropertyDescriptor(..)或Reflect.getOwnPropertyDescriptor(..))
defineProperty(..):通過[[DefineOwnProperty]],在代理上設置一個屬性描述符(Object.defineProperty(..)或Reflect.defineProperty(..))
getPrototypeOf(..):通過[[GetPrototypeOf]],取得代理的[[Prototype]](Object.getPrototypeOf(..),Reflect.getPrototypeOf(..),__proto__, Object#isPrototypeOf(..),或instanceof)
setPrototypeOf(..):通過[[SetPrototypeOf]],設置代理的[[Prototype]](Object.setPrototypeOf(..),Reflect.setPrototypeOf(..),或__proto__)
preventExtensions(..):通過[[PreventExtensions]]使代理成為不可擴展的(Object.preventExtensions(..)或Reflect.preventExtensions(..))
isExtensible(..):通過[[IsExtensible]],檢測代理的可擴展性(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)
提示: 關於每個這些元編程任務的更多信息,參見本章稍後的「Reflect API」一節。
關於將會觸發各種機關的動作,除了在前面列表中記載的以外,一些機關還會由另一個機關的默認動作間接地觸發。舉例來說:
varhandlers={
getOwnPropertyDescriptor(target,prop){
console.log(
"getOwnPropertyDescriptor"
);
returnObject.getOwnPropertyDescriptor(
target,prop
);
},
defineProperty(target,prop,desc){
console.log("defineProperty");
returnObject.defineProperty(
target,prop,desc
);
}
},
proxy=newProxy({},handlers);
proxy.a=2;
// getOwnPropertyDescriptor
// defineProperty
在設置一個屬性值時(不管是新添加還是更新),getOwnPropertyDescriptor(..)和defineProperty(..)處理器被默認的set(..)處理器觸發。如果你還定義了你自己的set(..)處理器,你或許對context(不是target!)進行了將會觸發這些代理機關的相應調用。
代理的限制
這些元編程處理器攔截了你可以對一個對象進行的範圍很廣泛的一組基礎操作。但是,有一些操作不能(至少是還不能)被用於攔截。
例如,從pobj代理到obj目標,這些操作全都沒有被攔截和轉送:
varobj={a:1,b:2},
handlers={..},
pobj=newProxy(obj,handlers);
typeofobj;
String(obj);
obj+"";
obj==pobj;
obj===pobj
也許在未來,更多這些語言中的底層基礎操作都將是可攔截的,那將給我們更多力量來從JavaScript自身擴展它。
警告: 對於代理處理器的使用來說存在某些 不變數 —— 它們的行為不能被覆蓋。例如,isExtensible(..)處理器的結果總是被強制轉換為一個boolean。這些不變數限制了一些你可以使用代理來自定義行為的能力,但是它們這樣做只是為了防止你創建奇怪和不尋常(或不合邏輯)的行為。這些不變數的條件十分複雜,所以我們就不再這裡全面闡述了,但是這篇博文(http://www.2ality.com/2014/12/es6-proxies.html#invariants)很好地講解了它們。
可撤銷的代理
一個一般的代理總是包裝著目標對象,而且在創建之後就不能修改了 —— 只要保持著一個指向這個代理的引用,代理的機制就將維持下去。但是,可能會有一些情況你想要創建一個這樣的代理:在你想要停止它作為代理時可以被停用。解決方案就是創建一個 可撤銷代理:
varobj={a:1},
handlers={
get(target,key,context){
// 注意:target === obj,
// context === pobj
console.log("accessing: ",key);
returntarget[key];
}
},
{proxy:pobj,revoke:prevoke}=
Proxy.revocable(obj,handlers);
pobj.a;
// accessing: a
// 1
// 稍後:
prevoke();
pobj.a;
// TypeError
一個可撤銷代理是由Proxy.revocable(..)創建的,它是一個普通的函數,不是一個像Proxy(..)那樣的構造器。此外,它接收同樣的兩個參數值:目標 和 處理器。
與new Proxy(..)不同的是,Proxy.revocable(..)的返回值不是代理本身。取而代之的是,它返回一個帶有 proxy 和 revoke 兩個屬性的對象 —— 我們使用了對象解構(參見第二章的「解構」)來將這些屬性分別賦值給變數pobj和prevoke。
一旦可撤銷代理被撤銷,任何訪問它的企圖(觸發它的任何機關)都將拋出TypeError。
一個使用可撤銷代理的例子可能是,將一個代理交給另一個存在於你應用中、並管理你模型中的數據的團體,而不是給它們一個指向正式模型對象本身的引用。如果你的模型對象改變了或者被替換掉了,你希望廢除這個你交出去的代理,以便於其他的團體能夠(通過錯誤!)知道要請求一個更新過的模型引用。
使用代理
這些代理處理器帶來的元編程的好處應當是顯而易見的。我們可以全面地攔截(而因此覆蓋)對象的行為,這意味著我們可以用一些非常強大的方式將對象行為擴展至JS核心之外。我們將看幾個模式的例子來探索這些可能性。
代理前置,代理後置
正如我們早先提到過的,你通常將一個代理考慮為一個目標對象的「包裝」。在這種意義上,代理就變成了代碼介面所針對的主要對象,而實際的目標對象則保持被隱藏/被保護的狀態。
你可能這麼做是因為你希望將對象傳遞到某個你不能完全「信任」的地方去,如此你需要在它的訪問權上強制實施一些特殊的規則,而不是傳遞這個對象本身。
考慮如下代碼:
varmessages=[],
handlers={
get(target,key){
// 是字元串值嗎?
if(typeoftarget[key]=="string"){
// 過濾掉標點符號
returntarget[key]
.replace(/[^w]/g,"");
}
// 讓其餘的東西通過
returntarget[key];
},
set(target,key,val){
// 僅設置唯一的小寫字元串
if(typeofval=="string"){
val=val.toLowerCase();
if(target.indexOf(val)==-1){
target.push(
val.toLowerCase()
);
}
}
returntrue;
}
},
messages_proxy=
newProxy(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!!
我稱此為 代理前置 設計,因為我們首先(主要、完全地)與代理進行互動。
我們在與messages_proxy的互動上強制實施了一些特殊規則,這些規則不會強制實施在messages本身上。我們僅在值是一個不重複的字元串時才將它添加為元素;我們還將這個值變為小寫。當從messages_proxy取得值時,我們過濾掉字元串中所有的標點符號。
另一種方式是,我們可以完全反轉這個模式,讓目標與代理交互而不是讓代理與目標交互。這樣,代碼其實只與主對象交互。達成這種後備方案的最簡單的方法是,讓代理對象存在於主對象的[[Prototype]]鏈中。
考慮如下代碼:
varhandlers={
get(target,key,context){
returnfunction(){
context.speak(key+"!");
};
}
},
catchall=newProxy({},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進行交互。當我們調用speak(..)時,它在greeter上被找到並直接使用。但當我們試圖訪問everyone()這樣的方法時,這個函數並不存在於greeter。
默認的對象屬性行為是向上檢查[[Prototype]]鏈(參見本系列的 this與對象原型),所以catchall被詢問有沒有一個everyone屬性。然後代理的get()處理器被調用並返回一個函數,這個函數使用被訪問的屬性名("everyone")調用speak(..)。
我稱這種模式為 代理後置,因為代理僅被用作最後一道防線。
"No Such Property/Method"
一個關於JS的常見的抱怨是,在你試著訪問或設置一個對象上還不存在的屬性時,默認情況下對象不是非常具有防禦性。你可能希望為一個對象預定義所有這些屬性/方法,而且在後續使用不存在的屬性名時拋出一個錯誤。
我們可以使用一個代理來達成這種想法,既可以使用 代理前置 也可以 代理後置 設計。我們將兩者都考慮一下。
varobj={
a:1,
foo(){
console.log("a:",this.a);
}
},
handlers={
get(target,key,context){
if(Reflect.has(target,key)){
returnReflect.get(
target,key,context
);
}
else{
throw"No such property/method!";
}
},
set(target,key,val,context){
if(Reflect.has(target,key)){
returnReflect.set(
target,key,val,context
);
}
else{
throw"No such property/method!";
}
}
},
pobj=newProxy(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(..)兩者,我們僅在目標對象的屬性已經存在時才轉送操作;否則拋出錯誤。代理對象應當是進行交互的主對象,因為它攔截這些操作來提供保護。
現在,讓我們考慮一下反過來的 代理後置 設計:
varhandlers={
get(){
throw"No such property/method!";
},
set(){
throw"No such property/method!";
}
},
pobj=newProxy({},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!
在處理器如何定義的角度上,這裡的 代理後置 設計相當簡單。與攔截[[Get]]和[[Set]]操作並僅在目標屬性存在時轉送它們不同,我們依賴於這樣一個事實:不管[[Get]]還是[[Set]]到達了我們的pobj後備對象,這個動作已經遍歷了整個[[Prototype]]鏈並且沒有找到匹配的屬性。在這時我們可以自由地、無條件地拋出錯誤。很酷,對吧?
代理黑入 [[Prototype]] 鏈
[[Get]]操作是[[Prototype]]機制被調用的主要渠道。當一個屬性不能在直接對象上找到時,[[Get]]會自動將操作交給[[Prototype]]對象。
這意味著你可以使用一個代理的get(..)機關來模擬或擴展這個[[Prototype]]機制的概念。
我們將考慮的第一種黑科技是創建兩個通過[[Prototype]]循環鏈接的對象(或者說,至少看起來是這樣!)。你不能實際創建一個真正循環的[[Prototype]]鏈,因為引擎將會拋出一個錯誤。但是代理可以假冒它!
考慮如下代碼:
varhandlers={
get(target,key,context){
if(Reflect.has(target,key)){
returnReflect.get(
target,key,context
);
}
// 假冒循環的 `[[Prototype]]`
else{
returnReflect.get(
target[
Symbol.for("[[Prototype]]")
],
key,
context
);
}
}
},
obj1=newProxy(
{
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();
}
}
);
// 假冒循環的 `[[Prototype]]` 鏈
obj1[Symbol.for("[[Prototype]]")]=obj2;
obj1.bar();
// bar: obj-1
// foo: obj-1
obj2.foo();
// foo: obj-2
注意: 為了讓事情簡單一些,在這個例子中我們沒有代理/轉送[[Set]]。要完整地模擬[[Prototype]]兼容,你會想要實現一個set(..)處理器,它在[[Prototype]]鏈上檢索一個匹配得屬性並遵循它的描述符的行為(例如,set,可寫性)。參見本系列的 this與對象原型。
在前面的代碼段中,obj2憑藉Object.create(..)語句[[Prototype]]鏈接到obj1。但是要創建反向(循環)的鏈接,我們在obj1的symbol位置Symbol.for("[[Prototype]]")(參見第二章的「Symbol」)上創建了一個屬性。這個symbol可能看起來有些特別/魔幻,但它不是的。它只是允許我使用一個被方便地命名的屬性,這個屬性在語義上看來是與我進行的任務有關聯的。
然後,代理的get(..)處理器首先檢查一個被請求的key是否存在於代理上。如果每個有,操作就被手動地交給存儲在target的Symbol.for("[[Prototype]]")位置中的對象引用。
這種模式的一個重要優點是,在obj1和obj2之間建立循環關係幾乎沒有入侵它們的定義。雖然前面的代碼段為了簡短而將所有的步驟交織在一起,但是如果你仔細觀察,代理處理器的邏輯完全是范用的(不具體地知道obj1或obj2)。所以,這段邏輯可以抽出到一個簡單的將它們連在一起的幫助函數中,例如setCircularPrototypeOf(..)。我們將此作為一個練習留給讀者。
現在我們看到了如何使用get(..)來模擬一個[[Prototype]]鏈接,但讓我們將這種黑科技推動的遠一些。與其製造一個循環[[Prototype]],搞一個多重[[Prototype]]鏈接(也就是「多重繼承」)怎麼樣?這看起來相當直白:
varobj1={
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)){
returnReflect.get(
target,key,context
);
}
// 假冒多重 `[[Prototype]]`
else{
for(varPoftarget[
Symbol.for("[[Prototype]]")
]){
if(Reflect.has(P,key)){
returnReflect.get(
P,key,context
);
}
}
}
}
},
obj3=newProxy(
{
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
注意: 正如在前面的循環[[Prototype]]例子後的注意中提到的,我們沒有實現set(..)處理器,但對於一個將[[Set]]模擬為普通[[Prototype]]行為的解決方案來說,它將是必要的。
obj3被設置為多重委託到obj1和obj2。在obj2.baz()中,this.foo()調用最終成為從obj1中抽出foo()(先到先得,雖然還有一個在obj2上的foo())。如果我們將連接重新排列為obj2, obj1,那麼obj2.foo()將被找到並使用。
同理,this.bar()調用沒有在obj1上找到bar(),所以它退而檢查obj2,這裡找到了一個匹配。
obj1和obj2代表obj3的兩個平行的[[Prototype]]鏈。obj1和/或obj2自身可以擁有委託至其他對象的普通[[Prototype]],或者自身也可以是多重委託的代理(就像obj3一樣)。
正如先前的循環[[Prototype]]的例子一樣,obj1,obj2和obj3的定義幾乎完全與處理多重委託的范用代理邏輯相分離。定義一個setPrototypesOf(..)(注意那個「s」!)這樣的工具將是小菜一碟,它接收一個主對象和一組模擬多重[[Prototype]]鏈接用的對象。同樣,我們將此作為練習留給讀者。
希望在這種種例子之後代理的力量現在變得明朗了。代理使得許多強大的元編程任務成為可能。
Reflect API
Reflect對象是一個普通對象(就像Math),不是其他內建原生類型那樣的函數/構造器。
它持有對應於你可以控制的各種元編程任務的靜態函數。這些函數與代理可以定義的處理器方法(機關)一一對應。
這些函數中的一些看起來與在Object上的同名函數很相似:
Reflect.getOwnPropertyDescriptor(..)
Reflect.defineProperty(..)
Reflect.getPrototypeOf(..)
Reflect.setPrototypeOf(..)
Reflect.preventExtensions(..)
Reflect.isExtensible(..)
這些工具一般與它們的Object.*對等物的行為相同。但一個區別是,Object.*對等物在它們的第一個參數值(目標對象)還不是對象的情況下,試圖將它強制轉換為一個對象。Reflect.*方法在同樣的情況下僅簡單地拋出一個錯誤。
一個對象的鍵可以使用這些工具訪問/檢測:
Reflect.ownKeys(..):返回一個所有直屬(不是「繼承的」)鍵的列表,正如被 Object.getOwnPropertyNames(..)和Object.getOwnPropertySymbols(..)返回的那樣。關於鍵的順序問題,參見「屬性枚舉順序」一節。
Reflect.enumerate(..):返回一個產生所有(直屬和「繼承的」)非symbol、可枚舉的鍵的迭代器(參見本系列的 this與對象原型)。 實質上,這組鍵與在for..in循環中被處理的那一組鍵是相同的。關於鍵的順序問題,參見「屬性枚舉順序」一節。
Reflect.has(..):實質上與用於檢查一個屬性是否存在於一個對象或它的[[Prototype]]鏈上的in操作符相同。例如,Reflect.has(o,"foo")實質上實施"foo" in o。
函數調用和構造器調用可以使用這些工具手動地實施,與普通的語法(例如,(..)和new)分開:
Reflect.apply(..):例如,Reflect.apply(foo,thisObj,[42,"bar"])使用thisObj作為foo(..)函數的this來調用它,並傳入參數值42和"bar"。
Reflect.construct(..):例如,Reflect.construct(foo,[42,"bar"])實質上調用new foo(42,"bar")。
對象屬性訪問,設置,和刪除可以使用這些工具手動實施:
Reflect.get(..):例如,Reflect.get(o,"foo")會取得o.foo。
Reflect.set(..):例如,Reflect.set(o,"foo",42)實質上實施o.foo = 42。
Reflect.deleteProperty(..):例如,Reflect.deleteProperty(o,"foo")實質上實施delete o.foo。
Reflect的元編程能力給了你可以模擬各種語法特性的程序化等價物,暴露以前隱藏著的抽象操作。例如,你可以使用這些能力來擴展 領域特定語言(DSL)的特性和API。
屬性順序
在ES6之前,羅列一個對象的鍵/屬性的順序沒有在語言規範中定義,而是依賴於具體實現的。一般來說,大多數引擎會以創建的順序來羅列它們,雖然開發者們已經被強烈建議永遠不要依仗這種順序。
在ES6中,羅列直屬屬性的屬性是由[[OwnPropertyKeys]]演算法定義的(ES6語言規範,9.1.12部分),它產生所有直屬屬性(字元串或symbol),不論其可枚舉性。這種順序僅對Reflect.ownKeys(..)有保證()。
這個順序是:
首先,以數字上升的順序,枚舉所有數字索引的直屬屬性。
然後,以創建順序枚舉剩下的直屬字元串屬性名。
最後,以創建順序枚舉直屬symbol屬性。
考慮如下代碼:
varo={};
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語言規範,9.1.11部分)從目標對象和它的[[Prototype]]鏈中僅產生可枚舉屬性。它被用於Reflect.enumerate(..)和for..in。可觀察到的順序是依賴於具體實現的,語言規範沒有控制它。
相比之下,Object.keys(..)調用[[OwnPropertyKeys]]演算法來得到一個所有直屬屬性的列表。但是,它過濾掉了不可枚舉屬性,然後特別為了JSON.stringify(..)和for..in而將這個列表重排,以匹配遺留的、依賴於具體實現的行為。所以通過擴展,這個順序 也 與Reflect.enumerate(..)的順序像吻合。
換言之,所有四種機制(Reflect.enumerate(..),Object.keys(..),for..in,和JSON.stringify(..))都同樣將與依賴於具體實現的順序像吻合,雖然技術上它們是以不同的方式達到的同樣的效果。
具體實現可以將這四種機制與[[OwnPropertyKeys]]的順序相吻合,但不是必須的。無論如何,你將很可能從它們的行為中觀察到以下的排序:
這一切可以歸納為:在ES6中,根據語言規範Reflect.ownKeys(..),Object.getOwnPropertyNames(..),和Object.getOwnPropertySymbols(..)保證都有可預見和可靠的順序。所以依賴於這種順序來建造代碼是安全的。
Reflect.enumerate(..),Object.keys(..),和for..in (擴展一下的話還有JSON.stringification(..))繼續互相共享一個可觀察的順序,就像它們往常一樣。但這個順序不一定與Reflect.ownKeys(..)的相同。在使用它們依賴於具體實現的順序時依然應當小心。
特性測試
什麼是特性測試?它是一種由你運行來判定一個特性是否可用的測試。有些時候,這種測試不僅是為了判定存在性,還是為判定對特定行為的適應性 —— 特性可能存在但有bug。
這是一種元編程技術 —— 測試你程序將要運行的環境然後判定你的程序應當如何動作。
在JS中特性測試最常見的用法是檢測一個API的存在性,而且如果它不存在,就定義一個填補(見第一章)。例如:
if(!Number.isNaN){
Number.isNaN=function(x){
returnx!==x;
};
}
在這個代碼段中的if語句就是一個元編程:我們探測我們的程序和它的運行時環境,來判定我們是否和如何進行後續處理。
但是如何測試一個涉及新語法的特性呢?
你可能會嘗試這樣的東西:
try{
a=()=>{};
ARROW_FUNCS_ENABLED=true;
}
catch(err){
ARROW_FUNCS_ENABLED=false;
}
不幸的是,這不能工作,因為我們的JS程序是要被編譯的。因此,如果引擎還沒有支持ES6箭頭函數的話,它就會在() => {}語法的地方熄火。你程序中的語法錯誤會阻止它的運行,進而阻止你程序根據特性是否被支持而進行後續的不同相應。
為了圍繞語法相關的特性進行特性測試的元編程,我們需要一個方法將測試與我們程序將要通過的初始編譯步驟隔離開。舉例來說,如果我們能夠將進行測試的代碼存儲在一個字元串中,之後JS引擎默認地將不會嘗試編譯這個字元串中的內容,直到我們要求它這麼做。
你的思路是不是跳到了使用eval(..)?
別這麼著急。看看本系列的 作用域與閉包 來了解一下為什麼eval(..)是一個壞主意。但是有另外一個缺陷較少的選項:Function(..)構造器。
考慮如下代碼:
try{
newFunction("( () => {} )");
ARROW_FUNCS_ENABLED=true;
}
catch(err){
ARROW_FUNCS_ENABLED=false;
}
好了,現在我們判定一個像箭頭函數這樣的特性是否 能 被當前的引擎所編譯來進行元編程。你可能會想知道,我們要用這種信息做什麼?
檢查API的存在性,並定義後備的API填補,對於特性檢測成功或失敗來說都是一條明確的道路。但是對於從ARROW_FUNCS_ENABLED是true還是false中得到的信息來說,我們能對它做什麼呢?
因為如果引擎不支持一種特性,它的語法就不能出現在一個文件中,所以你不能在這個文件中定義使用這種語法的函數。
你所能做的是,使用測試來判定你應當載入哪一組JS文件。例如,如果在你的JS應用程序中的啟動裝置中有一組這樣的特性測試,那麼它就可以測試環境來判定你的ES6代碼是否可以直接載入運行,或者你是否需要載入一個代碼的轉譯版本(參見第一章)。
這種技術稱為 分割投遞。
事實表明,你使用ES6編寫的JS程序有時可以在ES6+瀏覽器中完全「原生地」運行,但是另一些時候需要在前ES6瀏覽器中運行轉譯版本。如果你總是載入並使用轉譯代碼,即便是在新的ES6兼容環境中,至少是有些情況下你運行的也是次優的代碼。這並不理想。
分割投遞更加複雜和精巧,但對於你編寫的代碼和你的程序所必須在其中運行的瀏覽器支持的特性之間,它代表一種更加成熟和健壯的橋接方式。
FeatureTests.io
為所有的ES6+語法以及語義行為定義特性測試,是一項你可能不想自己解決的艱巨任務。因為這些測試要求動態編譯(new Function(..)),這會產生不幸的性能損耗。
另外,在每次你的應用運行時都執行這些測試可能是一種浪費,因為平均來說一個用戶的瀏覽器在幾周之內至多只會更新一次,而即使是這樣,新特性也不一定會在每次更新中都出現。
最終,管理一個對你特定代碼庫進行的特性測試列表 —— 你的程序將很少用到ES6的全部 —— 是很容易失控而且易錯的。
「https://featuretests.io」的「特性測試服務」為這種挫折提供了解決方案。
你可以將這個服務的庫載入到你的頁面中,而它會載入最新的測試定義並運行所有的特性測試。在可能的情況下,它將使用Web Worker的後台處理中這樣做,以降低性能上的開銷。它還會使用LocalStorage持久化來緩存測試的結果 —— 以一種可以被所有你訪問的使用這個服務的站點所共享的方式,這將及大地降低測試需要在每個瀏覽器實例上運行的頻度。
你可以在每一個用戶的瀏覽器上進行運行時特性測試,而且你可以使用這些測試結果動態地向用戶傳遞最適合他們環境的代碼(不多也不少)。
另外,這個服務還提供工具和API來掃描你的文件以判定你需要什麼特性,這樣你就能夠完全自動化你的分割投遞構建過程
對ES6的所有以及未來的部分進行特性測試,以確保對於任何給定的環境都只有最佳的代碼會被載入和運行 —— FeatureTests.io使這成為可能。
點擊展開全文


※你不懂JS:ES6與未來之元編程(下)
※你不懂JS:ES6與未來之元編程(上)
※JavaScript代碼風格要素
※二零一七之端午節
※Angular組件間通信
TAG:前端早讀課 |
※JSP的編程
※JSONP 編程
※SGU合格速報:摘3枚明治大學SGJS項目OFFER!
※HTML5+CSS3從入門到精通 CSS3及JS媒體查詢詳解
※新ANSI、ESDA、JEDEC JS-002 CDM測試標準概覽
※眾網友界定編程語言,JS、SQL和HTML到底算編程語言嗎?
※SGU合格速報:明治大學SGJS項目OFFER!
※關於 JDK 9 中的 JShell,你應該了解的 10 件事
※Spring MVC請求及返回JSON數據
※JSON、XML、TOML、CSON、YAML 大比拼
※Angular 垮台、ES6 最受歡迎,20,000 名程序員告訴你誰是 JS 王者!
※10分鐘了解JSON Web令牌
※JS001工藝變更前後在晚期NSCLC患者中的PK相似性研究
※PHP程序的JSON
※Angular垮台、ES6最受歡迎,20000名程序員告訴你誰是JS王者!
※JSON編程的parse() 方法
※在Python中使用JSON
※幾張圖為你分析HTML、JS與PHP之間的數據傳輸
※JSP 編程Session
※JSON 使用