ES6躬行記(24)——代理和反射
代理和反射是ES6新增的兩個特性,兩者之間是協調合作的關係,它們的具體功能將在接下來的章節中分別講解。
一、代理
ES6引入代理(Proxy)地目的是攔截物件的內建操作,注入自定義的邏輯,改變物件的預設行為。也就是說,將某些JavaScript內部的操作暴露了出來,給予開發人員更多的許可權。這其實是一種超程式設計(metaprogramming)的能力,即把程式碼看成資料,對程式碼進行程式設計,改變程式碼的行為。
在ES6中,代理是一種特殊的物件,如果要使用,需要像下面這樣先生成一個Proxy例項。
new Proxy(target, handler);
建構函式Proxy()有兩個引數,其中target是要用代理封裝的目標物件,handler也是一個物件,它的方法被稱為陷阱(trap),用於指定攔截後的行為。下面是一個代理的簡單示例。
var obj = {}, handler = { set(target, property, value, receiver) { target[property] = "hello " + value; } }, p = new Proxy(obj, handler); p.name = "strick"; console.log(p.name); //"hello strick"
在上面的程式碼中,p是一個Proxy例項,它的目標物件是obj,使用了屬性相關的陷阱:set()方法。當它寫入obj的name屬性時,會對其進行攔截,在屬性值之前加上“hello ”字首。除了上例使用的set()方法,ES6還給出了另外12種可用的陷阱,在後面的章節中會對它們做簡單的介紹。
1)陷阱
表12羅列了目前所有可用的陷阱,第二列表示當前陷阱可攔截的行為,注意,只挑選了其中的幾個用於展示。
表12 十三種陷阱
陷阱 | 攔截 | 返回值 |
get() | 讀取屬性 | 任意值 |
set() | 寫入屬性 | 布林值 |
has() | in運算子 | 布林值 |
deleteProperty() | delete運算子 | 布林值 |
getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor() | 屬性描述符物件 |
defineProperty() | Object.defineProperty() | 布林值 |
preventExtensions() | Object.preventExtensions() | 布林值 |
isExtensible() | Object.isExtensible() | 布林值 |
getPrototypeOf() |
Object.getPrototypeOf() __proto__ Object.prototype.isPrototypeOf() instanceof |
物件 |
setPrototypeOf() | Object.setPrototypeOf() | 布林值 |
apply() |
Function.prototype.apply() 函式呼叫 Function.prototype.call() |
任意值 |
construct() | new運算子作用於建構函式 | 物件 |
ownKeys() |
Object.getOwnPropertyNames() Object.keys() Object.getOwnPropertySymbols() for-in迴圈 |
陣列 |
目前支援的攔截就上面幾種,像typeof運算子、全等比較等操作還不被ES6支援。接下來會挑選其中的兩次個陷阱,講解它們的簡單應用。
在JavaScript中,當讀取物件上不存在的屬性時,不會報錯而是返回undefined,這其實在某些情況下會發生歧義,現在利用陷阱中的get()方法就能改變預設行為,如下所示。
var obj = { name: "strick" }, handler = { get(target, property, receiver) { if(property in target) return target[property]; throw "未定義的錯誤"; } }, p = new Proxy(obj, handler); p.name; //"strick" p.age; //未定義的錯誤
在get()方法中有3個引數,target是目標物件(即obj),property是讀取的屬性的名稱(即“name”和“age”),receiver是當前的Proxy例項(即p)。在讀取屬性時,會用in運算子判斷當前屬性是否存在,如果存在就返回相應的屬性值,否則就會丟擲錯誤,這樣就能避免歧義的出現。
在眾多陷阱中,只有apply()和construct()的目標物件得是函式。以apply()方法為例,它有3個引數,target是目標函式,thisArg是this的指向,argumentsList是函式的引數序列,它的具體使用如下所示。
function getName(name) { return name; } var obj = { prefix: "hello " }, handler = { apply(target, thisArg, argumentsList) { if(thisArg && thisArg.prefix) return target(thisArg.prefix + argumentsList[0]); return target(...argumentsList); } }, p = new Proxy(getName, handler); p("strick"); //"strick" p.call(obj, "strick"); //"hello strick"
p是一個Proxy例項,p("strick")是一次普通的函式呼叫,此時雖然攔截了,但是仍然會把引數原樣傳過去;而p.call(obj, "strick")是間接的函式呼叫,此時會給第一個引數新增字首,從而改變函式最終的返回值。
2)撤銷代理
Proxy.revocable()方法能夠建立一個可撤銷的代理,它能接收兩個引數,其含義與建構函式Proxy()中的相同,但返回值是一個物件,包含兩個屬性,如下所列。
(1)proxy:新生成的Proxy例項。
(2)revoke:撤銷函式,它沒有引數,能把與它一起生成的Proxy例項撤銷掉。
下面是一個簡單的示例,obj是目標物件,handler是陷阱物件,傳遞給Proxy.revocable()後,通過物件解構將返回值賦給了proxy和revoke兩個變數。
var obj = {}, handler = {}; let {proxy, revoke} = Proxy.revocable(obj, handler); revoke(); delete proxy.name; //型別錯誤 typeof proxy; //"object"
在呼叫revoke()函式後,就不能再對proxy進行攔截了。像上例使用delete運算子,就會丟擲型別錯誤,但像typeof之類的不可攔截的運算子還是可以成功執行的。
3)原型
代理可以成為其它物件的原型,就像下面這樣。
var obj = { name: "strick" }, handler = { get(target, property, receiver) { if(property == "name") return "hello " + target[property]; return true; } }, p = new Proxy({}, handler); Object.setPrototypeOf(obj, p); //obj的原型指向Proxy例項 obj.name; //"strick" obj.age; //true
p是一個Proxy例項,它會攔截屬性的讀取操作,obj的原型指向了p,注意,p的目標物件不是obj。當obj讀取name屬性時,不會觸發攔截,因為name是自有屬性,所以不會去原型上查詢,最終得到的結果是沒有字首的“strick”。之前的代理都是直接作用於相關物件(例如上面的obj),因此只要執行可攔截的動作就會被處理,但現在中間隔了個原型,有了更多的限制。而在讀取age屬性時,由於自有屬性中沒有它,因此就會去原型上查詢,從而觸發了攔截操作,返回了true。
二、反射
反射(Reflect)向外界暴露了一些底層操作的預設行為,它是一個沒有建構函式的內建物件,類似於Math物件,其所有方法都是靜態的。代理中的每個陷阱都會對應一個同名的反射方法(例如Reflect.set()、Reflect.ownKeys()等),而每個反射方法又都會關聯到對應代理所攔截的行為(例如in運算子、Object.defineProperty()等),這樣就能保證某個操作的預設行為可隨時被訪問到。反射讓物件的內建行為變得更加嚴謹、合理與便捷,具體表現如下所列。
(1)引數的檢驗更為嚴格,Object的getPrototypeOf()、isExtensible()等方法會將非物件的引數自動轉換成相應的物件(例如字串轉換成String物件,如下程式碼所示),而關聯的反射方法卻不會這麼做,它會直接丟擲型別錯誤。
Object.getPrototypeOf("strick") === String.prototype; //true Reflect.getPrototypeOf("strick"); //型別錯誤
(2)更合理的返回值,Object.setPrototypeOf()會返回它的第一個引數,而Reflect的同名方法會返回一個布林值,後者能更直觀的反饋設定是否成功,兩個方法的對比如下所示。
var obj = {}; Object.setPrototypeOf(obj, String) === obj; //true Reflect.setPrototypeOf(obj, String); //true
(3)用方法替代運算子,反射能以呼叫方法的形式完成new、in、delete等運算子的功能,在下面的示例中,先使用運算子,再給出對應的反射方法。
function func() { } new func(); Reflect.construct(func, []); var people = { name: "strick" }; "name" in people; Reflect.has(people, "name"); delete people["name"]; Reflect.deleteProperty(people, "name");
(4)避免冗長的方法呼叫,以apply()方法為例,如下所示。
Function.prototype.apply.call(Math.ceil, null, [2.5]); //3 Reflect.apply(Math.ceil, null, [2.5]); //3
上面程式碼的第一條語句比較繞,需要將其分解成兩部分:Function.prototype.apply()和call()。ES5規定apply()和call()兩個方法在最後都要呼叫一個有特殊功能的內部函式,如下程式碼所示,func引數表示呼叫這兩個方法的函式。
[[Call]](func, thisArg, argList)
內部函式的功能就是在呼叫func()函式時,傳遞給它的引數序列是argList,其內部的this指向了thisArg。當執行第一條語句時,傳遞給[[Call]]函式的三個引數如下所示。
[[Call]](Function.prototype.apply, Math.ceil, [null, [2.5]])
接下來會呼叫原型上的apply()方法,由於其this指向了Math.ceil(即當前呼叫apply()方法的是Math.ceil),因此[[Call]]函式的第一個引數就是Math.ceil,如下所示。
[[Call]](Math.ceil, null, [2.5]) //相當於 Math.ceil.apply(null, [2.5])
&n