簡單的復習下 ES6 的 Proxy、Reflect

來源 | https://segmentfault.com/a/1190000039956559
前言
Proxy (代理)
創(chuàng)建空代理
就是代碼中操作的是代理對象。
const target = {id: 'target'};const handler = {};const proxy = new Proxy(target, handler);// id 屬性會訪問同一個值console.log(target.id); // targetconsole.log(proxy.id); // target// 給目標屬性賦值會反映在兩個對象上// 因為兩個對象訪問的是同一個值target.id = 'foo';console.log(target.id); // fooconsole.log(proxy.id); // foo// 給代理屬性賦值會反映在兩個對象上// 因為這個賦值會轉(zhuǎn)移到目標對象proxy.id = 'bar';console.log(target.id); // barconsole.log(proxy.id); // bar
定義捕獲器
捕獲器可以理解為處理程序?qū)ο笾卸x的用來直接或間接在代理對象上使用的一種“攔截器”,每次在代理對象上調(diào)用這些基本操作時,代理可以在這些操作傳播到目標對象之前先調(diào)用捕獲器函數(shù),從而攔截并修改相應的行為。
const target = {foo: 'bar'};const handler = {// 捕獲器在處理程序?qū)ο笾幸苑椒麨殒Iget() {return 'handler override';}};const proxy = new Proxy(target, handler);console.log(target.foo); // barconsole.log(proxy.foo); // handler override
get() 捕獲器會接收到目標對象,要查詢的屬性和代理對象三個參數(shù)。我們可以對上述代碼進行如下改造。
const target = {foo: 'bar'};const handler = {// 捕獲器在處理程序?qū)ο笾幸苑椒麨殒Iget(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// trueconsole.log(proxy.foo); // barconsole.log(target.foo); // bar
處理程序?qū)ο笾兴锌梢圆东@的方法都有對應的反射(Reflect)API 方法。這些方法與捕獲器攔截的方法具有相同的名稱和函數(shù)簽名,而且也具有與被攔截方法相同的行為。因此,使用反射 API 也可以像下面這樣定義出空代理對象:
const target = {foo: 'bar'};const handler = {get() {// 第一種寫法return Reflect.get(...arguments);// 第二種寫法return Reflect.get}};const proxy = new Proxy(target, handler);console.log(proxy.foo); // barconsole.log(target.foo); // bar
我們也可以以此,來對將要訪問的屬性的返回值進行修飾。
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 youconsole.log(target.foo); // barconsole.log(proxy.baz); // quxconsole.log(target.baz); // qux
可撤銷代理
有時候可能需要中斷代理對象與目標對象之間的聯(lián)系。對于使用 new Proxy()創(chuàng)建的普通代理來說,這種聯(lián)系會在代理對象的生命周期內(nèi)一直持續(xù)存在。
Proxy 也暴露了 revocable()方法,這個方法支持撤銷代理對象與目標對象的關(guān)聯(lián)。撤銷代理的操作是不可逆的。而且,撤銷函數(shù)(revoke())是冪等的,調(diào)用多少次的結(jié)果都一樣。撤銷代理之后再調(diào)用代理會拋出 TypeError。
const target = {foo: 'bar'};const handler = {get() {return 'intercepted';}};const { proxy, revoke } = Proxy.revocable(target, handler);console.log(proxy.foo); // interceptedconsole.log(target.foo); // barrevoke();console.log(proxy.foo); // TypeError
代理另一個代理
代理可以攔截反射 API 的操作,而這意味著完全可以創(chuàng)建一個代理,通過它去代理另一個代理。這樣就可以在一個目標對象之上構(gòu)建多層攔截網(wǎng):
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、代理中的this
const target = {thisValEqualsProxy() {return this === proxy;}}const proxy = new Proxy(target, {});console.log(target.thisValEqualsProxy()); // falseconsole.log(proxy.thisValEqualsProxy()); // true
這樣看起來并沒有什么問題,this指向調(diào)用者。但是如果目標對象依賴于對象標識,那就可能碰到意料之外的問題。
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); // 123const userInstanceProxy = new Proxy(user, {});console.log(userInstanceProxy.id); // undefined
這是因為 User 實例一開始使用目標對象作為 WeakMap 的鍵,代理對象卻嘗試從自身取得這個實例。
要解決這個問題,就需要重新配置代理,把代理 User 實例改為代理 User 類本身。之后再創(chuàng)建代
理的實例就會以代理實例作為 WeakMap 的鍵了:
const UserClassProxy = new Proxy(User, {});const proxyUser = new UserClassProxy(456);console.log(proxyUser.id);
2、代理與內(nèi)部槽位
在代理Date類型時:根據(jù) ECMAScript 規(guī)范,Date 類型方法的執(zhí)行依賴 this 值上的內(nèi)部槽位[[NumberDate]]。代理對象上不存在這個內(nèi)部槽位,而且這個內(nèi)部槽位的值也不能通過普通的 get()和 set()操作訪問到,于是代理攔截后本應轉(zhuǎn)發(fā)給目標對象的方法會拋出 TypeError:
const target = new Date();const proxy = new Proxy(target, {});console.log(proxy instanceof Date); // trueproxy.getDate(); // TypeError: 'this' is not a Date object
Reflect(反射)
Reflect對象與Proxy對象一樣,也是 ES6 為了操作對象而提供的新 API。Reflect的設計目的:
將Object對象的一些明顯屬于語言內(nèi)部的方法(比如Object.defineProperty),放到Reflect對象上。
修改某些Object方法的返回結(jié)果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false。
讓Object操作都變成函數(shù)行為。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函數(shù)行為。
Reflect對象的方法與Proxy對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。這就讓Proxy對象可以方便地調(diào)用對應的Reflect方法,完成默認行為,作為修改行為的基礎。也就是說,不管Proxy怎么修改默認行為,你總可以在Reflect上獲取默認行為。
代理與反射API
get()
接收參數(shù):
target:目標對象。
property:引用的目標對象上的字符串鍵屬性。
receiver:代理對象或繼承代理對象的對象。
返回:返回值無限制
get()捕獲器會在獲取屬性值的操作中被調(diào)用。對應的反射 API 方法為 Reflect.get()。
const myTarget = {};const proxy = new Proxy(myTarget, {get(target, property, receiver) {console.log('get()');return Reflect.get(...arguments)}});proxy.foo;// get()
set()
接收參數(shù):
target:目標對象。
property:引用的目標對象上的字符串鍵屬性。
value:要賦給屬性的值。
receiver:接收最初賦值的對象。
返回:返回 true 表示成功;返回 false 表示失敗,嚴格模式下會拋出 TypeError。
set()捕獲器會在設置屬性值的操作中被調(diào)用。對應的反射 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()
接收參數(shù):
target:目標對象。
property:引用的目標對象上的字符串鍵屬性。
返回:
has()必須返回布爾值,表示屬性是否存在。返回非布爾值會被轉(zhuǎn)型為布爾值。
has()捕獲器會在 in 操作符中被調(diào)用。對應的反射 API 方法為 Reflect.has()。
const myTarget = {};const proxy = new Proxy(myTarget, {has(target, property) {console.log('has()');return Reflect.has(...arguments)}});'foo' in proxy;// has()
defineProperty()
Reflect.defineProperty方法基本等同于Object.defineProperty,用來為對象定義屬性。
接收參數(shù):
target:目標對象。
property:引用的目標對象上的字符串鍵屬性。
descriptor:包含可選的 enumerable、configurable、writable、value、get 和 set定義的對象。
返回:
defineProperty()必須返回布爾值,表示屬性是否成功定義。返回非布爾值會被轉(zhuǎn)型為布爾值。
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()
getOwnPropertyDescriptor()
Reflect.getOwnPropertyDescriptor基本等同于Object.getOwnPropertyDescriptor,用于得到指定屬性的描述對象。
接收參數(shù):
target:目標對象。
property:引用的目標對象上的字符串鍵屬性。
返回:
getOwnPropertyDescriptor()必須返回對象,或者在屬性不存在時返回 undefined。
const myTarget = {};const proxy = new Proxy(myTarget, {getOwnPropertyDescriptor(target, property) {console.log('getOwnPropertyDescriptor()');return Reflect.getOwnPropertyDescriptor(...arguments)}});Object.getOwnPropertyDescriptor(proxy, 'foo');// getOwnPropertyDescriptor()
deleteProperty()
Reflect.deleteProperty方法等同于delete obj[name],用于刪除對象的屬性。
接收參數(shù):
target:目標對象。
property:引用的目標對象上的字符串鍵屬性。
返回:
deleteProperty()必須返回布爾值,表示刪除屬性是否成功。返回非布爾值會被轉(zhuǎn)型為布爾值。
ownKeys()
Reflect.ownKeys方法用于返回對象的所有屬性,基本等同于Object.getOwnPropertyNames與Object.getOwnPropertySymbols之和。
接收參數(shù):
target:目標對象。
返回:
ownKeys()必須返回包含字符串或符號的可枚舉對象。
getPrototypeOf()
Reflect.getPrototypeOf方法用于讀取對象的__proto__屬性
接收參數(shù):
target:目標對象。
返回:
getPrototypeOf()必須返回對象或 null。
等等。。
代理模式
跟蹤屬性訪問
通過捕獲 get、set 和 has 等操作,可以知道對象屬性什么時候被訪問、被查詢。把實現(xiàn)相應捕獲器的某個對象代理放到應用中,可以監(jiān)控這個對象何時在何處被訪問過:
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 nameproxy.age = 27; // Setting age=27
隱藏屬性
代理的內(nèi)部實現(xiàn)對外部代碼是不可見的,因此要隱藏目標對象上的屬性也輕而易舉。
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); // undefinedconsole.log(proxy.bar); // undefinedconsole.log(proxy.baz); // 3// has()console.log('foo' in proxy); // falseconsole.log('bar' in proxy); // falseconsole.log('baz' in proxy); // true
屬性驗證
因為所有賦值操作都會觸發(fā) set()捕獲器,所以可以根據(jù)所賦的值決定是允許還是拒絕賦值:
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); // 1proxy.onlyNumbersGoHere = '2';console.log(proxy.onlyNumbersGoHere); // 1
函數(shù)與構(gòu)造函數(shù)參數(shù)驗證
跟保護和驗證對象屬性類似,也可對函數(shù)和構(gòu)造函數(shù)參數(shù)進行審查。比如,可以讓函數(shù)只接收某種類型的值:
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)); // 4console.log(proxy(4, '7', 1));// Error: Non-number argument provided類似地,可以要求實例化時必須給構(gòu)造函數(shù)傳參: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
數(shù)據(jù)綁定與可觀察對象
通過代理可以把運行時中原本不相關(guān)的部分聯(lián)系到一起。這樣就可以實現(xiàn)各種模式,從而讓不同的代碼互操作。
比如,可以將被代理的類綁定到一個全局實例集合,讓所有創(chuàng)建的實例都被添加到這個集合中:
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{}]
另外,還可以把集合綁定到一個事件分派程序,每次插入新實例時都會發(fā)送消息:
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');// Johnproxy.push('Jacob');// Jacob
使用 Proxy 實現(xiàn)觀察者模式
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
結(jié)尾
本文主要參考阮一峰es6教程、js紅寶書第四版。
學習更多技能
請點擊下方公眾號
![]()
