代理和反射是ES6新增的兩個特性,兩者之間是協(xié)調(diào)合作的關(guān)系,它們的具體功能將在接下來的章節(jié)中分別講解。
一、代理
ES6引入代理(Proxy)地目的是攔截對象的內(nèi)置操作,注入自定義的邏輯,改變對象的默認行為。也就是說,將某些JavaScript內(nèi)部的操作暴露了出來,給予開發(fā)人員更多的權(quán)限。這其實是一種元編程(metaprogramming)的能力,即把代碼看成數(shù)據(jù),對代碼進行編程,改變代碼的行為。
在ES6中,代理是一種特殊的對象,如果要使用,需要像下面這樣先生成一個Proxy實例。
new Proxy(target, handler);
構(gòu)造函數(shù)Proxy()有兩個參數(shù),其中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,使用了屬性相關(guān)的陷阱:set()方法。當它寫入obj的name屬性時,會對其進行攔截,在屬性值之前加上“hello
”前綴。除了上例使用的set()方法,ES6還給出了另外12種可用的陷阱,在后面的章節(jié)中會對它們做簡單的介紹。
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()? ? ? ? ? ?函數(shù)調(diào)用
?Function.prototype.call()
?任意值
?construct() ?new運算符作用于構(gòu)造函數(shù) ?對象
?ownKeys()
?Object.getOwnPropertyNames()? ??Object.keys()
?Object.getOwnPropertySymbols()??for-in循環(huán)
?數(shù)組
目前支持的攔截就上面幾種,像typeof運算符、全等比較等操作還不被ES6支持。接下來會挑選其中的兩次個陷阱,講解它們的簡單應用。
在JavaScript中,當讀取對象上不存在的屬性時,不會報錯而是返回undefined,這其實在某些情況下會發(fā)生歧義,現(xiàn)在利用陷阱中的get()方法就能改變默認行為,如下所示。
var obj = { name: "strick" }, handler = { get(target, property, receiver) { if
(propertyin target) return target[property]; throw "未定義的錯誤"; } }, p = new
Proxy(obj, handler); p.name; //"strick" p.age; //未定義的錯誤
在get()方法中有3個參數(shù),target是目標對象(即obj),property是讀取的屬性的名稱(即“name”和“age”),receiver是當前的Proxy實例(即p)。在讀取屬性時,會用in運算符判斷當前屬性是否存在,如果存在就返回相應的屬性值,否則就會拋出錯誤,這樣就能避免歧義的出現(xiàn)。?
?
在眾多陷阱中,只有apply()和construct()的目標對象得是函數(shù)。以apply()方法為例,它有3個參數(shù),target是目標函數(shù),thisArg是this的指向,argumentsList是函數(shù)的參數(shù)序列,它的具體使用如下所示。
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")是一次普通的函數(shù)調(diào)用,此時雖然攔截了,但是仍然會把參數(shù)原樣傳過去;而p.call(obj,
"strick")是間接的函數(shù)調(diào)用,此時會給第一個參數(shù)添加前綴,從而改變函數(shù)最終的返回值。
2)撤銷代理
Proxy.revocable()方法能夠創(chuàng)建一個可撤銷的代理,它能接收兩個參數(shù),其含義與構(gòu)造函數(shù)Proxy()中的相同,但返回值是一個對象,包含兩個屬性,如下所列。
(1)proxy:新生成的Proxy實例。
(2)revoke:撤銷函數(shù),它沒有參數(shù),能把與它一起生成的Proxy實例撤銷掉。
下面是一個簡單的示例,obj是目標對象,handler是陷阱對象,傳遞給Proxy.revocable()后,通過對象解構(gòu)將返回值賦給了proxy和revoke兩個變量。
var obj = {}, handler = {}; let {proxy, revoke} = Proxy.revocable(obj,
handler); revoke();delete proxy.name; //類型錯誤 typeof proxy; //"object"
在調(diào)用revoke()函數(shù)后,就不能再對proxy進行攔截了。像上例使用delete運算符,就會拋出類型錯誤,但像typeof之類的不可攔截的運算符還是可以成功執(zhí)行的。
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屬性時,不會觸發(fā)攔截,因為name是自有屬性,所以不會去原型上查找,最終得到的結(jié)果是沒有前綴的“strick”。之前的代理都是直接作用于相關(guān)對象(例如上面的obj),因此只要執(zhí)行可攔截的動作就會被處理,但現(xiàn)在中間隔了個原型,有了更多的限制。而在讀取age屬性時,由于自有屬性中沒有它,因此就會去原型上查找,從而觸發(fā)了攔截操作,返回了true。
二、反射
反射(Reflect)向外界暴露了一些底層操作的默認行為,它是一個沒有構(gòu)造函數(shù)的內(nèi)置對象,類似于Math對象,其所有方法都是靜態(tài)的。代理中的每個陷阱都會對應一個同名的反射方法(例如Reflect.set()、Reflect.ownKeys()等),而每個反射方法又都會關(guān)聯(lián)到對應代理所攔截的行為(例如in運算符、Object.defineProperty()等),這樣就能保證某個操作的默認行為可隨時被訪問到。反射讓對象的內(nèi)置行為變得更加嚴謹、合理與便捷,具體表現(xiàn)如下所列。
?。?)參數(shù)的檢驗更為嚴格,Object的getPrototypeOf()、isExtensible()等方法會將非對象的參數(shù)自動轉(zhuǎn)換成相應的對象(例如字符串轉(zhuǎn)換成String對象,如下代碼所示),而關(guān)聯(lián)的反射方法卻不會這么做,它會直接拋出類型錯誤。
Object.getPrototypeOf("strick") === String.prototype; //true
Reflect.getPrototypeOf("strick"); //類型錯誤
?。?)更合理的返回值,Object.setPrototypeOf()會返回它的第一個參數(shù),而Reflect的同名方法會返回一個布爾值,后者能更直觀的反饋設(shè)置是否成功,兩個方法的對比如下所示。
var obj = {}; Object.setPrototypeOf(obj, String) === obj; //true
Reflect.setPrototypeOf(obj, String); //true
?。?)用方法替代運算符,反射能以調(diào)用方法的形式完成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)避免冗長的方法調(diào)用,以apply()方法為例,如下所示。
Function.prototype.apply.call(Math.ceil, null, [2.5]); //3
Reflect.apply(Math.ceil,null, [2.5]); //3
上面代碼的第一條語句比較繞,需要將其分解成兩部分:Function.prototype.apply()和call()。ES5規(guī)定apply()和call()兩個方法在最后都要調(diào)用一個有特殊功能的內(nèi)部函數(shù),如下代碼所示,func參數(shù)表示調(diào)用這兩個方法的函數(shù)。
[[Call]](func, thisArg, argList)
內(nèi)部函數(shù)的功能就是在調(diào)用func()函數(shù)時,傳遞給它的參數(shù)序列是argList,其內(nèi)部的this指向了thisArg。當執(zhí)行第一條語句時,傳遞給[[Call]]函數(shù)的三個參數(shù)如下所示。
[[Call]](Function.prototype.apply, Math.ceil, [null, [2.5]])
接下來會調(diào)用原型上的apply()方法,由于其this指向了Math.ceil(即當前調(diào)用apply()方法的是Math.ceil),因此[[Call]]函數(shù)的第一個參數(shù)就是Math.ceil,如下所示。
[[Call]](Math.ceil, null, [2.5]) //相當于 Math.ceil.apply(null, [2.5])
?
熱門工具 換一換