第九章 代理和反射
3个要点
1、代理基础
2、代码捕获器与反射方法
3、代理模式
9.1 代理基础
代理是目标对象的抽象,类似于C++中的指针,因为它可以用作目标对象的替身,但又完全独立于目标对象。
9.1.1 创建空代理
最简单的代理就是空代理,即抽象一个目标对象,什么也不做。默认情况下,在代理对象上执行的素有操作都会五拽的传播到目标对象上。因此,在任何可以使用目标对象的地方,都可以通过同样的方式来使用与之关联的代理对象。
代理是通过Proxy构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象,缺少一个都会抛出TypeError的异常
sample 1 改变对象,指针也改变
const target={id:'thisISID'};const handler={};const proxy=new Proxy(target,handler);//id属性会访问同一个值console.log(target.id); //返回 'thisISID'console.log(proxy.id); //返回 'thisISID'//改变目标对象的属性,指针也会改变target.id="我要变了";console.log(target.id); //返回 '我要变了'console.log(proxy.id); //返回 '我要变了'
hasOwnProperty()
console.log(target.hasOwnProperty('id')); //返回trueconosle.log(proxy.hasOwnProperty('id')); //返回true
不能用 instanceof
console.log(target instanceof Proxy); //TypeErrorconsole.log(proxy instacneof Proxy); //TypeError
9.1.2 定义捕获器
使用代理的目的就是可以定义捕获器(trap)捕获器就是在处理程序对象中定义的“基本操作的拦截器”。
sample
const target={foo:"bar"};const handler={//捕获器在处理程序对象中以方法名键get(){return '不允许读';}};const proxy=new Proxy(target,handler);console.log(proxy.foo); //被get 拦截,不允许读console.log(target.foo); //返回 bar 在目标对象上不受影响
9.1.3 捕获器参数和反射API
所有捕获器都可以访问相应的参数,基于这些参数可以被重建捕获方法的原始行为。get()捕获器会接收目标对象,查询的属性和代理对象三个参数
const target={name:"JackMa",age:59,job:"福报厂创始人"};const handler={get(trapTarget,property,receiver){console.log(trapTarget===target);console.log(property);console.log(receiver===proxy);}};const proxy=new Proxy(target,handler);proxy.name; //返回true name true
通过上面的捕获器就可以重建被捕获方法的原始行为
const target={name:"JackMa",age:59,job:"福报厂创始人"};const handler={get(trapTarget,property,receiver){console.log(trapTarget===target);console.log(trapTarget[property]);console.log(receiver==proxy);return trapTarget[property];}}console.log(proxy.name); //返回 true JackMa true JackMaconosle.log(target.name); //返回 JackMa
所有捕获器都可以基于自己的参数重建原始操作,但并非所有的捕获器行为都像get()那么简单。因此通过手动写代码时不现实的。实际上可以通过调用全局Reflect对象上(封装了原始行为)的同名方法来轻松重建
处理程序对象中所有可以被捕获的方法都有相应的反射(Reflect) API方法。这些方法与捕获器的方法加油相同的名称和函数签名,而且也具有与被拦截的方法相同的行为。
反射的例子
let person ={name:"JackMA",age:59,job:'福报厂CEO'};let handler={get(){return Reflect.get(...arguments);}};const proxy=new Proxy(target,handler);console.log(proxy.name); //返回JackMaconsole.log(person.name); //返回JackMa
9.1.4 捕获器不变式
使用捕获器几乎可以改变所有基本方法的行为,但也不是没有限制。根据ECMAScipt规范,每个捕获的方法都知道目标对象的上下文、捕获函数签名。而捕获处理程序的行为必须遵守”捕获器不变式(trap invariant)”
比如,如果一个对象又一个补课配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,就抛出TypeError
let person={};Object.defineProperty(person,'name',{configurable:false,writable:false,value:"JackMa"});let handler={get(){return "XXX";}};let handler2={get(){return "JackMa";}}let proxy=new Proxy(person,handler);console.log(proxy.name); //返回TypeErrorlet proxy2=new Proxy(person,handler2);console.log(proxy2.name); //返回 JackMa
9.1.5 可撤销代理 revoke()函数
有时候可能需要终端代理对象与目标对象之间的联系。使用new Proxy()创建的普通代理来说,这种联系会在代理对象的生命周期内一直存在。
Proxy也暴露了revocable()方法,这个方法支持撤销代理与目标对象的关系。
revoke()时幂等的,调用多少次的结果都一样。撤销代理之后再调用代理会抛出TypeError
const person={name:'JackMa'};const handler={get(){return 'this is a ret';}};const {proxy,revoke}=Proxy.revocable(person,handler);cosole.log(proxy.name); //返回 this is a retcosole.log(person.name); //返回 JackMarevoke();console.log(proxy.name); //返回TypeError
9.1.6 实用反射API
某些情况下应该优先使用反射API ,这是由一定的理由的。
1、反射API与对象API
在使用反射API时,要记住:
(1)、反射API并不限于捕获处理程度。
(2)、大多数反射API方法在Object类型上有对应的方法。
通常,Object上的方法适用于通用程序,而反射方法适用于细粒度的对象控件与操作。
2、状态标记
很多反射方法返回称作”状态标记”的布尔值,表示意图执行的操作是否成功。有时候,状态标记比那些返回修改后的对象或者抛出错误(取决于方法)的反射API方法更有用。
例如,可以使用反射API对下面的代码进行重构
//初始化代码const o={};try{Object.defineProperty(o,'name','JackMa');console.log('sucess');}catch(e){console.log('failure');}//这里返回 failure
在定义新属性时如果发生问题,Reflect.defineProperty()会返回false,而不是抛出错误。因此使用这个反射方法可以重构上面代码
//重构后的代码let person={};if(Reflect.defineProperty(person,'name','JackMa')){console.log('sucess!');}else{console.log('failure');}//返回 sucessconsole.log(person.name); //返回JackMa
以下反射方法都会提供状态标记:
1、类方法 Reflect.preventExtensions(target)
此方法是阻止新属性添加到对象上去
let person={};Reflect.preventExtension(person);if(Reflect.defineProperty(person,'name','JackMa')){console.log('Sucess!');}else{console.log('failure!');}//返回 failure!
2、类方法 Reflect.deleteProperty(target,propertyKey);
9.1.7 代理另一个代理
代理可以拦截反射API的操作,而着以为着完全可以创建一个代理,通过它去代理另一个代理,这样可以在一个目标上构建多个拦截网。
let person={name:"JackMa",age:59};let firstProxy=new Proxy(target,{get(){console.log('first Proxy');return Reflect.get(...arguments);}});const secondProxy=new Proxy(firstProxy,{get(){console.log('第二次代理');return Reflect.get(...arguments);}})console.log(secondProxy.name);//返回 第二次代理 first Proxy JackMa
9.1.8 代理的问题与不足
1、代理中this指向问题
this 指向调用这个方法的对象
let person={thisValEqu(){return this===proxy;}}const proxy=new Proxy(person,{});console.log(person.thisValEqu()); //返回falseconsole.log(person.thisValEqu()); //返回true
2、代理与内部槽位
代理和内置引用类型,比如Array的实例可以很好的协同。但有些ECMAScript内置类型可能会依赖代理无法控制的机制,结果导致代理上的某些错误
let timi=new Date();const proxy=new Proxy(timi,{});console.log(proxy istance of Date); //返回trueproxy.getDate(); //TypeError
9.2 代理捕获器与反射方法
代理可以捕获13种不同的操作方法,这些操作有各自不同的反射API方法。参数,关联ECMAScript操作和不变式
9.2.1 get()
get()捕获器会在获取属性值的操作中被调用。对应的反射API方法为Reflect.get()
let person={name:"JackMa",}let proxy=new Proxy(person,{get(tar,prop,rec){console.log(tar);console.log(prop);console.log(receiver);console.log(tar[prop]);return Reflect.get(...arguments);}});proxy.name; //返回 Object name proxy JackMa
1、返回值
返回值无限制
2、拦截的操作
1、proxy.property
2、proxy[property]
3、Object.create(proxy)[property]
4、Reflect.GET(proxy,property,receiver)
3、捕获器处理程序参数
target:目标对象
property:引用的目标对象上的属性
receiver:代理对象或者继承代理对象的对象
4、捕获器不变式
如果target.property不可写且不可配置,则处理程序放回的值必须与target.property匹配
9.2.2 set()
set()捕获器会在设置属性值的操作中被调用。对应的反射API方法为Reflect.set()
let person={name="Timi"}let proxy=new Proxy(person,{set(target,property,value,receiver){console.log('set()');return Reflect.set(...arguments);}});proxy.name="JackMa"; //返回 set()console.log(proxy.name); //返回 JackMa
1、返回值
返回true表示成功;返回false表示失败,严格模式下会抛出TypeError
2、拦截操作
proxy.property=value
proxy[property]=value
Object.create(proxy)[property]=value
Reflect.set(proxy,property,value,receiver);
3、捕获器处理程序参数
target 目标对象 property 对象上的属性
value 要赋值的值 receiver 接收最初赋值的对象
9.2.3 has()
has()捕获器会在in操作父中被调用。对应的反射API方法为Reflect.has()
let person={name:'JackMa',}let proxy=new Proxy(person,{has(target,property){console.log('Has');return Reflect.has(...arguments);}});console.log('JackMa' in proxy); //返回falseconsole.log('name' in proxy); //返回true
9.2.4defineProperty()
defineProperty()捕获器会在Object.defineProperty()中被调用。对应的反射API方法为Reflect.defineProperty()
let person={};const proxy=new Proxy(person,{defineProperty(target,property,descriptor){console.log('defineProperty()');return Reflect.defineProperty(...arguments)}});Object.defineProperty(proxy,'name',{value:'JackMa'}); //返回trueconsole.log(proxy.name); //返回Jack Ma
9.2.5getOwnPropertyDescriptor()
9.3代理模式(暂时略过)
使用代理可以在代码中实现一些有用的编程模式
9.3.1 跟踪属性访问
通过捕获get、set和has等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获器的某个对象代理放到应用中,可以监控这个对象合适再何处被访问
