快速掌握ES6的Proxy、Reflect

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

