ES6新增的代理
和反射
爲開發者提供了攔截並向基本操做嵌入額外行爲的能力
。具體地說,能夠給目標對象定義一個關聯的代理對象,而這個代理對象能夠做爲抽象的目標對象來使用。在對目標對象的各類操做影響目標對象以前,能夠在代理對象中對這些操做加以控制。es6
代理是使用 Proxy
構造函數建立的。這個構造函數接收兩個參數:目標對象和處理程序對象。缺乏其中任何一個參數都會拋出 TypeError。app
以下面的代碼所示,在代理對象上執行的任何操做實際上都會應用到目標對象。惟一可感知的不一樣
就是代碼中操做的是代理對象。ide
const target = { id: 'target' }; const handler = {}; const proxy = new Proxy(target, handler); // id 屬性會訪問同一個值 console.log(target.id); // target console.log(proxy.id); // target // 給目標屬性賦值會反映在兩個對象上 // 由於兩個對象訪問的是同一個值 target.id = 'foo'; console.log(target.id); // foo console.log(proxy.id); // foo // 給代理屬性賦值會反映在兩個對象上 // 由於這個賦值會轉移到目標對象 proxy.id = 'bar'; console.log(target.id); // bar console.log(proxy.id); // bar
捕獲器能夠理解爲處理程序對象中定義的用來直接或間接在代理對象上使用的一種「攔截器」,每次在代理對象上調用這些基本操做時,代理能夠在這些操做傳播到目標對象以前先調用捕獲器函數,從而攔截並修改相應的行爲。函數
const target = { foo: 'bar' }; const handler = { // 捕獲器在處理程序對象中以方法名爲鍵 get() { return 'handler override'; } }; const proxy = new Proxy(target, handler); console.log(target.foo); // bar console.log(proxy.foo); // handler override
get()
捕獲器會接收到目標對象,要查詢的屬性和代理對象三個參數。咱們能夠對上述代碼進行以下改造this
const target = { foo: 'bar' }; const handler = { // 捕獲器在處理程序對象中以方法名爲鍵 get(trapTarget, property, receiver) { console.log(trapTarget === target); console.log(property); console.log(receiver === proxy); return trapTarget[property] } }; const proxy = new Proxy(target, handler); proxy.foo; // true // foo // true console.log(proxy.foo); // bar console.log(target.foo); // bar
處理程序對象中全部能夠捕獲的方法都有對應的反射(Reflect)API 方法。這些方法與捕獲器攔截的方法具備相同的名稱和函數簽名,並且也具備與被攔截方法相同的行爲。所以,使用反射 API 也能夠像下面這樣定義出空代理對象:spa
const target = { foo: 'bar' }; const handler = { get() { // 第一種寫法 return Reflect.get(...arguments); // 第二種寫法 return Reflect.get } }; const proxy = new Proxy(target, handler); console.log(proxy.foo); // bar console.log(target.foo); // bar
咱們也能夠以此,來對將要訪問的屬性的返回值
進行修飾。.net
const target = { foo: 'bar', baz: 'qux' }; const handler = { get(trapTarget, property, receiver) { let decoration = ''; if (property === 'foo') { decoration = ' I love you'; } return Reflect.get(...arguments) + decoration; } }; const proxy = new Proxy(target, handler); console.log(proxy.foo); // bar I love you console.log(target.foo); // bar console.log(proxy.baz); // qux console.log(target.baz); // qux
有時候可能須要中斷代理對象與目標對象之間的聯繫。對於使用 new Proxy()建立的普通代理來講,這種聯繫會在代理對象的生命週期內一直持續存在。Proxy 也暴露了 revocable()
方法,這個方法支持撤銷代理對象與目標對象的關聯。撤銷代理的操做是不可逆的。並且,撤銷函數(revoke()
)是冪等的,調用多少次的結果都同樣。撤銷代理以後再調用代理會拋出 TypeError。設計
const target = { foo: 'bar' }; const handler = { get() { return 'intercepted'; } }; const { proxy, revoke } = Proxy.revocable(target, handler); console.log(proxy.foo); // intercepted console.log(target.foo); // bar revoke(); console.log(proxy.foo); // TypeError
代理能夠攔截反射 API 的操做,而這意味着徹底能夠建立一個代理,經過它去代理另外一個代理。這樣就能夠在一個目標對象之上構建多層攔截網:代理
const target = { foo: 'bar' }; const firstProxy = new Proxy(target, { get() { console.log('first proxy'); return Reflect.get(...arguments); } }); const secondProxy = new Proxy(firstProxy, { get() { console.log('second proxy'); return Reflect.get(...arguments); } }); console.log(secondProxy.foo); // second proxy // first proxy // bar
1. 代理中的thiscode
const target = { thisValEqualsProxy() { return this === proxy; } } const proxy = new Proxy(target, {}); console.log(target.thisValEqualsProxy()); // false console.log(proxy.thisValEqualsProxy()); // true
這樣看起來並無什麼問題,this指向調用者。可是若是目標對象依賴於對象標識,那就可能碰到意料以外的問題。
const wm = new WeakMap(); class User { constructor(userId) { wm.set(this, userId); } set id(userId) { wm.set(this, userId); } get id() { return wm.get(this); } } const user = new User(123); console.log(user.id); // 123 const userInstanceProxy = new Proxy(user, {}); console.log(userInstanceProxy.id); // undefined
這是由於 User 實例一開始使用目標對象做爲 WeakMap 的鍵,代理對象卻嘗試從自身取得這個實
例。要解決這個問題,就須要從新配置代理,把代理 User 實例改成代理 User 類自己。以後再建立代
理的實例就會以代理實例做爲 WeakMap 的鍵了:
const UserClassProxy = new Proxy(User, {}); const proxyUser = new UserClassProxy(456); console.log(proxyUser.id);
2. 代理與內部槽位
在代理Date類型時:根據 ECMAScript 規範,Date 類型方法的執行依賴 this 值上的內部槽位[[NumberDate]]。代理對象上不存在這個內部槽位,並且這個內部槽位的值也不能經過普通的 get()和 set()操做訪問到,因而代理攔截後本應轉發給目標對象的方法會拋出 TypeError:
const target = new Date(); const proxy = new Proxy(target, {}); console.log(proxy instanceof Date); // true proxy.getDate(); // TypeError: 'this' is not a Date object
Reflect
對象與Proxy
對象同樣,也是 ES6 爲了操做對象而提供的新 API。Reflect的設計目的:
接收參數:
const myTarget = {}; const proxy = new Proxy(myTarget, { get(target, property, receiver) { console.log('get()'); return Reflect.get(...arguments) } }); proxy.foo; // get()
接收參數:
set()捕獲器會在設置屬性值的操做中被調用。對應的反射 API 方法爲 Reflect.set()。
const myTarget = {}; const proxy = new Proxy(myTarget, { set(target, property, value, receiver) { console.log('set()'); return Reflect.set(...arguments) } }); proxy.foo = 'bar'; // set()
接收參數:
返回:
has()捕獲器會在 in 操做符中被調用。對應的反射 API 方法爲 Reflect.has()。
const myTarget = {}; const proxy = new Proxy(myTarget, { has(target, property) { console.log('has()'); return Reflect.has(...arguments) } }); 'foo' in proxy; // has()
Reflect.defineProperty方法基本等同於Object.defineProperty,用來爲對象定義屬性。
接收參數:
返回:
const myTarget = {}; const proxy = new Proxy(myTarget, { defineProperty(target, property, descriptor) { console.log('defineProperty()'); return Reflect.defineProperty(...arguments) } }); Object.defineProperty(proxy, 'foo', { value: 'bar' }); // defineProperty()
Reflect.getOwnPropertyDescriptor基本等同於Object.getOwnPropertyDescriptor,用於獲得指定屬性的描述對象。
接收參數:
返回:
const myTarget = {}; const proxy = new Proxy(myTarget, { getOwnPropertyDescriptor(target, property) { console.log('getOwnPropertyDescriptor()'); return Reflect.getOwnPropertyDescriptor(...arguments) } }); Object.getOwnPropertyDescriptor(proxy, 'foo'); // getOwnPropertyDescriptor()
Reflect.deleteProperty方法等同於delete obj[name],用於刪除對象的屬性。
接收參數:
返回:
Reflect.ownKeys方法用於返回對象的全部屬性,基本等同於Object.getOwnPropertyNames與Object.getOwnPropertySymbols之和。
接收參數:
返回:
Reflect.getPrototypeOf方法用於讀取對象的__proto__屬性
接收參數:
返回:
等等。。
經過捕獲 get、set 和 has 等操做,能夠知道對象屬性何時被訪問、被查詢。把實現相應捕獲器的某個對象代理放到應用中,能夠監控這個對象什麼時候在何處被訪問過:
const user = { name: 'Jake' }; const proxy = new Proxy(user, { get(target, property, receiver) { console.log(`Getting ${property}`); return Reflect.get(...arguments); }, set(target, property, value, receiver) { console.log(`Setting ${property}=${value}`); return Reflect.set(...arguments); } }); proxy.name; // Getting name proxy.age = 27; // Setting age=27
代理的內部實現對外部代碼是不可見的,所以要隱藏目標對象上的屬性也垂手可得。
const hiddenProperties = ['foo', 'bar']; const targetObject = { foo: 1, bar: 2, baz: 3 }; const proxy = new Proxy(targetObject, { get(target, property) { if (hiddenProperties.includes(property)) { return undefined; } else { return Reflect.get(...arguments); } }, has(target, property) { if (hiddenProperties.includes(property)) { return false; } else { return Reflect.has(...arguments); } } }); // get() console.log(proxy.foo); // undefined console.log(proxy.bar); // undefined console.log(proxy.baz); // 3 // has() console.log('foo' in proxy); // false console.log('bar' in proxy); // false console.log('baz' in proxy); // true
由於全部賦值操做都會觸發 set()捕獲器,因此能夠根據所賦的值決定是容許仍是拒絕賦值:
const target = { onlyNumbersGoHere: 0 }; const proxy = new Proxy(target, { set(target, property, value) { if (typeof value !== 'number') { return false; } else { return Reflect.set(...arguments); } } }); proxy.onlyNumbersGoHere = 1; console.log(proxy.onlyNumbersGoHere); // 1 proxy.onlyNumbersGoHere = '2'; console.log(proxy.onlyNumbersGoHere); // 1
跟保護和驗證對象屬性相似,也可對函數和構造函數參數進行審查。好比,可讓函數只接收某種類型的值:
function median(...nums) { return nums.sort()[Math.floor(nums.length / 2)]; } const proxy = new Proxy(median, { apply(target, thisArg, argumentsList) { for (const arg of argumentsList) { if (typeof arg !== 'number') { throw 'Non-number argument provided'; } } return Reflect.apply(...arguments); } }); console.log(proxy(4, 7, 1)); // 4 console.log(proxy(4, '7', 1)); // Error: Non-number argument provided 相似地,能夠要求實例化時必須給構造函數傳參: class User { constructor(id) { this.id_ = id; } } const proxy = new Proxy(User, { construct(target, argumentsList, newTarget) { if (argumentsList[0] === undefined) { throw 'User cannot be instantiated without id'; } else { return Reflect.construct(...arguments); } } }); new proxy(1); new proxy(); // Error: User cannot be instantiated without id
經過代理能夠把運行時中本來不相關的部分聯繫到一塊兒。這樣就能夠實現各類模式,從而讓不一樣的代碼互操做。好比,能夠將被代理的類綁定到一個全局實例集合,讓全部建立的實例都被添加到這個集合中:
const userList = []; class User { constructor(name) { this.name_ = name; } } const proxy = new Proxy(User, { construct() { const newUser = Reflect.construct(...arguments); userList.push(newUser); return newUser; } }); new proxy('John'); new proxy('Jacob'); new proxy('Jingleheimerschmidt'); console.log(userList); // [User {}, User {}, User{}]
另外,還能夠把集合綁定到一個事件分派程序,每次插入新實例時都會發送消息:
const userList = []; function emit(newValue) { console.log(newValue); } const proxy = new Proxy(userList, { set(target, property, value, receiver) { const result = Reflect.set(...arguments); if (result) { emit(Reflect.get(target, property, receiver)); } return result; } }); proxy.push('John'); // John proxy.push('Jacob'); // Jacob
const queuedObservers = new Set(); const observe = fn => queuedObservers.add(fn); const observable = obj => new Proxy(obj, {set}); function set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); queuedObservers.forEach(observer => observer()); return result; } const person = observable({ name: '張三', age: 20 }); function print() { console.log(`${person.name}, ${person.age}`) } observe(print); person.name = '李四'; // 輸出 // 李四, 20
本文主要參考阮一峯es6教程、js紅寶書第四版
因爲本人水平有限,若有錯誤,敬請與我聯繫指出,謝謝。