代理和反射是ES6新增的兩個特性,二者之間是協調合做的關係,它們的具體功能將在接下來的章節中分別講解。編程
ES6引入代理(Proxy)地目的是攔截對象的內置操做,注入自定義的邏輯,改變對象的默認行爲。也就是說,將某些JavaScript內部的操做暴露了出來,給予開發人員更多的權限。這實際上是一種元編程(metaprogramming)的能力,即把代碼當作數據,對代碼進行編程,改變代碼的行爲。數組
在ES6中,代理是一種特殊的對象,若是要使用,須要像下面這樣先生成一個Proxy實例。app
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種可用的陷阱,在後面的章節中會對它們作簡單的介紹。this
1)陷阱spa
表12羅列了目前全部可用的陷阱,第二列表示當前陷阱可攔截的行爲,注意,只挑選了其中的幾個用於展現。prototype
表12 十三種陷阱代理
陷阱 | 攔截 | 返回值 |
get() | 讀取屬性 | 任意值 |
set() | 寫入屬性 | 布爾值 |
has() | in運算符 | 布爾值 |
deleteProperty() | delete運算符 | 布爾值 |
getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor() | 屬性描述符對象 |
defineProperty() | Object.defineProperty() | 布爾值 |
preventExtensions() | Object.preventExtensions() | 布爾值 |
isExtensible() | Object.isExtensible() | 布爾值 |
getPrototypeOf() | Object.getPrototypeOf() __proto__code 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])