代理(Proxy)能夠攔截並改變 JS 引擎的底層操做,如數據讀取、屬性定義、函數構造等一系列操做。ES6 經過對這些底層內置對象的代理陷阱和反射函數,讓開發者能進一步接近 JS 引擎的能力。
什麼是代理和反射呢?
代理是用來替代另外一個對象(target),JS 經過new Proxy()
建立一個目標對象的代理,該代理與該目標對象表面上能夠被看成同一個對象來對待。javascript
當目標對象上的進行一些特定的底層操做時,代理容許你攔截這些操做而且覆寫它,而這本來只是 JS 引擎的內部能力。java
若是你對些代理&反射的概念比較困惑的話, 能夠直接看後面的應用示例,最後再從新看這些定義就會更清晰!
攔截行爲使用了一個可以響應特定操做的函數( 被稱爲陷阱),每一個代理陷阱對應一個反射(Reflect)方法。segmentfault
ES6 的反射 API 以 Reflect
對象的形式出現,對象每一個方法都與對應的陷阱函數同名,而且接收的參數也與之一致。如下是 Reflect
對象的一些方法:數組
代理陷阱 | 覆寫的特性 | 方法 | |
---|---|---|---|
get | 讀取一個屬性的值 | Reflect.get() | |
set | 寫入一個屬性 | Reflect.set() | |
has | in 運算符 | Reflect.has() | |
deleteProperty | delete 運算符 | Reflect.deleteProperty() | |
getPrototypeOf | Object.getPrototypeOf() | Reflect.getPrototypeOf() | |
isExtensible | Object.isExtensible() | Reflect.isExtensible() | |
defineProperty | Object.defineProperty() | Reflect.defineProperty | |
apply | 調用一個函數 | Reflect.apply() | |
construct | 使用 new 調用一個函數 | Reflect.construct() |
每一個陷阱函數均可以重寫 JS 對象的一個特定內置行爲,容許你攔截並修改它。安全
綜合來講,想要控制或改變JS的一些底層操做,能夠先建立一個代理對象,在這個代理對象上掛載一些陷阱函數,陷阱函數裏面有反射方法。經過接下來的應用示例能夠更清晰的明白代理的過程。app
當你使用 Proxy 構造器來建立一個代理時,須要傳遞兩個參數:目標對象(target)以及一個處理器( handler),函數
先建立一個僅進行傳遞的代理以下:測試
// 目標對象 let target = {}; // 代理對象 let proxy = new Proxy(target, {}); proxy.name = "hello"; console.log(proxy.name); // "hello" console.log(target.name); // "hello" target.name = "world"; console.log(proxy.name); // "world" console.log(target.name); // "world
上例中的 proxy 代理對象將全部操做直接傳遞給 target 目標對象,代理對象 proxy 自身並無存儲該屬性,它只是簡單將值傳遞給 target 對象,proxy.name 與 target.name 的屬性值老是相等,由於它們都指向 target.name。this
此時代理陷阱的處理器爲空對象,固然處理器能夠定義了一個或多個陷阱函數。雙向綁定
假設你想要建立一個對象,並要求其屬性值只能是數值,這就意味着該對象的每一個新增屬性
都要被驗證,而且在屬性值不爲數值類型時應當拋出錯誤。
這時須要使用 set 陷阱函數來攔截傳入的 value,該陷阱函數能接受四個參數:
set 陷阱對應的反射方法和默認特性是Reflect.set()
,和陷阱函數同樣接受這四個參數,並會基於操做是否成功而返回相應的結果:
let targetObj = {}; let proxyObj = new Proxy(targetObj, { set: set }); /* 定義 set 陷阱函數 */ function set (trapTarget, key, value, receiver) { if (isNaN(value)) { throw new TypeError("Property " + key + " must be a number."); } return Reflect.set(trapTarget, key, value, receiver); } /* 測試 */ proxyObj.count = 123; console.log(proxyObj.count); // 123 console.log(targetObj.count); // 123 proxyObj.anotherName = "proxy" // TypeError: Property anotherName must be a number.
示例中set 陷阱函數成功攔截傳入的 value 值,你能夠嘗試一下,若是註釋或不return Reflect.set()
會發生什麼?,答案是攔截陷阱就不會有反射響應。
須要注意的是,直接給 targetObj 目標對象賦值時是不會觸發 set 代理陷阱的,須要經過給代理對象賦值纔會觸發 set 代理陷阱與反射。
JS 很是有趣的特性之一,是讀取不存在的屬性時並不會拋出錯誤,而會把undefined
看成該屬性的值。
對於大型的代碼庫,當屬性名稱存在書寫錯誤時(不會拋錯)會致使嚴重的問題。這時使用 get 代理陷阱驗證對象結構(Object Shape),訪問不存在的屬性時就拋出錯誤,使對象結構驗證變得簡單。
get 陷阱函數會在讀取屬性時被調用,即便該屬性在對象中並不存在,它能接受三個參數:
Reflect.get()
方法接受與之相同的參數,並返回默認屬性的默認值。
let proxyObj = new Proxy(targetObj, { set: set, get: get }); /* 定義 get 陷阱函數 */ function get(trapTarget, key, receiver) { if (!(key in receiver)) { throw new TypeError("Property " + key + " doesn't exist."); } return Reflect.get(trapTarget, key, receiver); } console.log(proxyObj.count); // 123 console.log(proxyObj.newcount) // TypeError: Property newcount doesn't exist.
這段代碼容許添加新的屬性,而且此後能夠正常讀取該屬性的值,但當讀取的屬性並
不存在時,程序拋出了一個錯誤,而不是將其默認爲undefined
。
in
運算符,使用 deleteProperty 陷阱函數避免屬性被delete
刪除。注:in
運算符用於判斷對象中是否存在某個屬性,若是自有屬性或原型屬性匹配這個名稱字符串或Symbol,那麼in
運算符返回 true。
targetObj = { name: 'targetObject' }; console.log("name" in targetObj); // true console.log("toString" in targetObj); // true
其中 name 是對象自身的屬性,而 toString 則是原型屬性( 從 Object 對象上繼承而來),因此檢測結果都爲 true。
has 陷阱函數會在使用in
運算符時被調用,而且會傳入兩個參數(同名反射Reflect.has()
方法也同樣):
Symbol
符號類型)deleteProperty 陷阱函數會在使用delete
運算符去刪除對象屬性時下被調用,而且也會被傳入兩個參數(Reflect.deleteProperty()
方法也接受這兩個參數):
一些思考:分析過 Vue 源碼的都瞭解過,給一個 Vue 實例中掛載的 data,是經過
Object.defineProperty
代理 vm._data 中的對象屬性,實現雙向綁定...... 同理能夠考慮使用 ES6 的 Proxy 的 get 和 set 陷阱實現這個代理。
ES5 最重要的特徵之一就是引入了 Object.defineProperty()
方法定義屬性的特性。屬性的特性是爲了實現javascript引擎用的,屬於內部值,所以不能直接訪問他們。
屬性分爲數據屬性和訪問器屬性。使用Object.defineProperty()
方法修改數據屬性的特性值的示例以下:
let obj1 = { name: 'myobj', } /* 數據屬性*/ Object.defineProperty(obj1,'name',{ configurable: false, // default true writable: false, // default true enumerable: true, // default true value: 'jenny' // default undefined }) console.log(obj1.name) // 'jenny'
其中[[Configurable]]
表示可否經過 delete 刪除屬性從而從新定義爲訪問器屬性;[[Enumerable]]
表示可否經過for-in
循環返回屬性;[[Writable]]
表示可否修改屬性的值; [[Value]]
包含這個屬性的數據值。
對於訪問器屬性,該屬性不包含數據值,包含一對getter和setter函數,定義訪問器屬性必須使用Object.defineProperty()
方法:
let obj2 = { age: 18 } /* 訪問器屬性 */ Object.defineProperty(obj2,'_age',{ configurable: false, // default true enumerable: false, // default true get () { // default undefined return this.age }, set (num) { // default undefined this.age = num } }) /* 修改訪問器屬性調用 getter */ obj2._age = 20 console.log(obj2.age) // 20 /* 輸出訪問器屬性 */ console.log(Object.getOwnPropertyDescriptor(obj2,'_age')) // { get: [Function: get], // set: [Function: set], // enumerable: false, // configurable: false }
[[Get]]
在讀取屬性時調用的函數, [[Set]]
再寫入屬性時調用的函數。使用訪問器屬性的經常使用方式,是設置一個屬性的值致使其餘屬性發生變化。
代理容許你使用 defineProperty 同名函數陷阱函數攔截Object.defineProperty()
的調用,defineProperty 陷阱函數接受下列三個參數:
defineProperty 陷阱函數要求在操做後返回一個布爾值用於判斷操做是否成功,若是返回了 false 則拋出錯誤,故可使用該功能來限制哪些屬性能夠被Object.defineProperty() 方法定義。
例如,若是想阻止定義Symbol
符號類型的屬性,你能夠檢查傳入的屬性值,如果則返回 false:
/* 定義代理 */ let proxy = new Proxy({}, { defineProperty(trapTarget, key, descriptor) { if (typeof key === "symbol") { return false; } return Reflect.defineProperty(trapTarget, key, descriptor); } }); Object.defineProperty(proxy, "name", { value: "proxy" }); console.log(proxy.name); // "proxy" let nameSymbol = Symbol("name"); // 拋出錯誤 Object.defineProperty(proxy, nameSymbol, { value: "proxy" })
函數的兩個內部方法:[[Call]]
與[[Construct]]
會在函數被調用時調用,經過代理函數來爲這兩個內部方法設置陷阱,從而控制函數的行爲。
[[Construct]]
會在函數被使用new
運算符調用時執行,代理觸發construct()
陷阱函數,並和Reflect.construct()
同樣接收到下列兩個參數:
[[Call]]
會在函數被直接調用時執行,代理觸發apply()
陷阱函數,它和Reflect.apply()
都接收三個參數:
每一個函數都包含call()
和apply()
方法,用於重置函數運行的做用域即 this 指向,區別只是接收參數的方式不一樣:call()
的參數須要逐個列舉、apply()
是參數數組。
顯然,apply 與 construct 要求代理目標對象必須是一個函數,這兩個代理陷阱在函數的執行方式上開啓了不少的可能性,結合使用就能夠徹底控制任意的代理目標函數的行爲。
看到apply()
和construct()
陷阱的參數都有被傳遞給函數的參數數組argumentsList
,因此能夠用來驗證函數的參數。
例如須要保證全部參數都是某個特定類型的,而且不能經過 new 構造使用,示例以下:
/* 定義 sum 目標函數 */ function sum(...values) { return values.reduce((previous, current) => previous + current, 0); } /* 定義 apply 陷阱函數 */ function applyRef (trapTarget, thisArg, argumentList) { argumentList.forEach((arg) => { if (typeof arg !== "number") { throw new TypeError("All arguments must be numbers."); } }); return Reflect.apply(trapTarget, thisArg, argumentList); } /* 定義 construct 陷阱函數 */ function constructRef () { throw new TypeError("This function can't be called with new."); } /* 定義 sumProxy 代理函數 */ let sumProxy = new Proxy(sum, { apply: applyRef, construct: constructRef }); console.log(sumProxy(1, 2, 3, 4)); // 10 // console.log(sumProxy(1, "2", 3, 4)); // TypeError: All arguments must be numbers. // let result = new sumProxy() // TypeError: This function can't be called with new.
sum() 函數會將全部傳遞進來的參數值相加,此代碼經過將 sum() 函數封裝在 sumProxy() 代理中,若是傳入參數的值不是數值類型,該函數仍然會嘗試加法操做,但在函數運行以前攔截了函數調用,觸發apply
陷阱函數以保證每一個參數都是數值。
出於安全的考慮,這段代碼使用 construct
陷阱拋出錯誤,以確保該函數不會被使用 new 運算符調用
實例對象 instance 對象會被同時斷定爲 proxy 與 target 對象的實例,是由於 instanceof 運算符使用了原型鏈來進行推斷,而原型鏈查找並無受到這個代理的影響,所以 proxy 對象與 target 對象對於 JS 引擎來講就有同一個原型。
ES6 中新引入了class
類的概念,類使用constructor
構造函數封裝數據,並規定必須始終使用 new 來調用,緣由是類構造器的內部方法 [[Call]] 被明
確要求拋出錯誤。
代理能夠攔截對於 [[Call]] 方法的調用,你能夠藉助代理調用的類構造器。例如在缺乏 new 的狀況下建立一個新實例,就使用 apply 陷阱函數實現:
class Person { constructor(name) { this.name = name; } } let PersonProxy = new Proxy(Person, { apply: function(trapTarget, thisArg, argumentList) { return new trapTarget(...argumentList); } }); let me = PersonProxy("Jenny"); console.log(me.name); // "Jenny" console.log(me instanceof Person); // true console.log(me instanceof PersonProxy); // true
類構造器即類的構造函數,使用代理時它的行爲就像函數同樣,apply
陷阱函數重寫了默認的構造行爲。
關於類的更多有趣的用法,可參考 【ES6】更易於繼承的類語法
總結來講,代理的用途很是普遍,由於它提供了修改 JS 內置對象的全部行爲的入口。上述例子只是簡單的一些應用入門,還有更多複雜的示例,推薦閱讀《深刻理解ES6》。