第九章 代理和反射

3个要点

1、代理基础

2、代码捕获器与反射方法

3、代理模式

9.1 代理基础

代理是目标对象的抽象,类似于C++中的指针,因为它可以用作目标对象的替身,但又完全独立于目标对象。

9.1.1 创建空代理

最简单的代理就是空代理,即抽象一个目标对象,什么也不做。默认情况下,在代理对象上执行的素有操作都会五拽的传播到目标对象上。因此,在任何可以使用目标对象的地方,都可以通过同样的方式来使用与之关联的代理对象。

代理是通过Proxy构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象,缺少一个都会抛出TypeError的异常

sample 1 改变对象,指针也改变

  1. const target={
  2. id:'thisISID'
  3. };
  4. const handler={};
  5. const proxy=new Proxy(target,handler);
  6. //id属性会访问同一个值
  7. console.log(target.id); //返回 'thisISID'
  8. console.log(proxy.id); //返回 'thisISID'
  9. //改变目标对象的属性,指针也会改变
  10. target.id="我要变了";
  11. console.log(target.id); //返回 '我要变了'
  12. console.log(proxy.id); //返回 '我要变了'

hasOwnProperty()

  1. console.log(target.hasOwnProperty('id')); //返回true
  2. conosle.log(proxy.hasOwnProperty('id')); //返回true

不能用 instanceof

  1. console.log(target instanceof Proxy); //TypeError
  2. console.log(proxy instacneof Proxy); //TypeError

9.1.2 定义捕获器

使用代理的目的就是可以定义捕获器(trap)捕获器就是在处理程序对象中定义的“基本操作的拦截器”。

sample

  1. const target={
  2. foo:"bar"
  3. };
  4. const handler={
  5. //捕获器在处理程序对象中以方法名键
  6. get(){
  7. return '不允许读';
  8. }
  9. };
  10. const proxy=new Proxy(target,handler);
  11. console.log(proxy.foo); //被get 拦截,不允许读
  12. console.log(target.foo); //返回 bar 在目标对象上不受影响

9.1.3 捕获器参数和反射API

所有捕获器都可以访问相应的参数,基于这些参数可以被重建捕获方法的原始行为。get()捕获器会接收目标对象,查询的属性和代理对象三个参数

  1. const target={
  2. name:"JackMa",
  3. age:59,
  4. job:"福报厂创始人"
  5. };
  6. const handler={
  7. get(trapTarget,property,receiver){
  8. console.log(trapTarget===target);
  9. console.log(property);
  10. console.log(receiver===proxy);
  11. }
  12. };
  13. const proxy=new Proxy(target,handler);
  14. proxy.name; //返回true name true

通过上面的捕获器就可以重建被捕获方法的原始行为

  1. const target={
  2. name:"JackMa",
  3. age:59,
  4. job:"福报厂创始人"
  5. };
  6. const handler={
  7. get(trapTarget,property,receiver){
  8. console.log(trapTarget===target);
  9. console.log(trapTarget[property]);
  10. console.log(receiver==proxy);
  11. return trapTarget[property];
  12. }
  13. }
  14. console.log(proxy.name); //返回 true JackMa true JackMa
  15. conosle.log(target.name); //返回 JackMa

所有捕获器都可以基于自己的参数重建原始操作,但并非所有的捕获器行为都像get()那么简单。因此通过手动写代码时不现实的。实际上可以通过调用全局Reflect对象上(封装了原始行为)的同名方法来轻松重建

处理程序对象中所有可以被捕获的方法都有相应的反射(Reflect) API方法。这些方法与捕获器的方法加油相同的名称和函数签名,而且也具有与被拦截的方法相同的行为。

反射的例子

  1. let person ={
  2. name:"JackMA",
  3. age:59,
  4. job:'福报厂CEO'
  5. };
  6. let handler={
  7. get(){
  8. return Reflect.get(...arguments);
  9. }
  10. };
  11. const proxy=new Proxy(target,handler);
  12. console.log(proxy.name); //返回JackMa
  13. console.log(person.name); //返回JackMa

9.1.4 捕获器不变式

使用捕获器几乎可以改变所有基本方法的行为,但也不是没有限制。根据ECMAScipt规范,每个捕获的方法都知道目标对象的上下文、捕获函数签名。而捕获处理程序的行为必须遵守”捕获器不变式(trap invariant)”

比如,如果一个对象又一个补课配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,就抛出TypeError

  1. let person={};
  2. Object.defineProperty(person,'name',{
  3. configurable:false,
  4. writable:false,
  5. value:"JackMa"
  6. });
  7. let handler={
  8. get(){
  9. return "XXX";
  10. }
  11. };
  12. let handler2={
  13. get(){
  14. return "JackMa";
  15. }
  16. }
  17. let proxy=new Proxy(person,handler);
  18. console.log(proxy.name); //返回TypeError
  19. let proxy2=new Proxy(person,handler2);
  20. console.log(proxy2.name); //返回 JackMa

9.1.5 可撤销代理 revoke()函数

有时候可能需要终端代理对象与目标对象之间的联系。使用new Proxy()创建的普通代理来说,这种联系会在代理对象的生命周期内一直存在。

Proxy也暴露了revocable()方法,这个方法支持撤销代理与目标对象的关系。

revoke()时幂等的,调用多少次的结果都一样。撤销代理之后再调用代理会抛出TypeError

  1. const person={
  2. name:'JackMa'
  3. };
  4. const handler={
  5. get(){
  6. return 'this is a ret';
  7. }
  8. };
  9. const {proxy,revoke}=Proxy.revocable(person,handler);
  10. cosole.log(proxy.name); //返回 this is a ret
  11. cosole.log(person.name); //返回 JackMa
  12. revoke();
  13. console.log(proxy.name); //返回TypeError

9.1.6 实用反射API

某些情况下应该优先使用反射API ,这是由一定的理由的。

1、反射API与对象API

在使用反射API时,要记住:

(1)、反射API并不限于捕获处理程度。

(2)、大多数反射API方法在Object类型上有对应的方法。

通常,Object上的方法适用于通用程序,而反射方法适用于细粒度的对象控件与操作。

2、状态标记

很多反射方法返回称作”状态标记”的布尔值,表示意图执行的操作是否成功。有时候,状态标记比那些返回修改后的对象或者抛出错误(取决于方法)的反射API方法更有用。

例如,可以使用反射API对下面的代码进行重构

  1. //初始化代码
  2. const o={};
  3. try{
  4. Object.defineProperty(o,'name','JackMa');
  5. console.log('sucess');
  6. }catch(e){
  7. console.log('failure');
  8. }
  9. //这里返回 failure

在定义新属性时如果发生问题,Reflect.defineProperty()会返回false,而不是抛出错误。因此使用这个反射方法可以重构上面代码

  1. //重构后的代码
  2. let person={};
  3. if(Reflect.defineProperty(person,'name','JackMa')){
  4. console.log('sucess!');
  5. }else{
  6. console.log('failure');
  7. }
  8. //返回 sucess
  9. console.log(person.name); //返回JackMa

以下反射方法都会提供状态标记:

1、类方法 Reflect.preventExtensions(target)

此方法是阻止新属性添加到对象上去

  1. let person={};
  2. Reflect.preventExtension(person);
  3. if(Reflect.defineProperty(person,'name','JackMa')){
  4. console.log('Sucess!');
  5. }else{
  6. console.log('failure!');
  7. }
  8. //返回 failure!

2、类方法 Reflect.deleteProperty(target,propertyKey);

9.1.7 代理另一个代理

代理可以拦截反射API的操作,而着以为着完全可以创建一个代理,通过它去代理另一个代理,这样可以在一个目标上构建多个拦截网。

  1. let person={
  2. name:"JackMa",
  3. age:59
  4. };
  5. let firstProxy=new Proxy(target,{
  6. get(){
  7. console.log('first Proxy');
  8. return Reflect.get(...arguments);
  9. }
  10. });
  11. const secondProxy=new Proxy(firstProxy,{
  12. get(){
  13. console.log('第二次代理');
  14. return Reflect.get(...arguments);
  15. }
  16. })
  17. console.log(secondProxy.name);
  18. //返回 第二次代理 first Proxy JackMa

9.1.8 代理的问题与不足

1、代理中this指向问题

this 指向调用这个方法的对象

  1. let person={
  2. thisValEqu(){
  3. return this===proxy;
  4. }
  5. }
  6. const proxy=new Proxy(person,{});
  7. console.log(person.thisValEqu()); //返回false
  8. console.log(person.thisValEqu()); //返回true

2、代理与内部槽位

代理和内置引用类型,比如Array的实例可以很好的协同。但有些ECMAScript内置类型可能会依赖代理无法控制的机制,结果导致代理上的某些错误

  1. let timi=new Date();
  2. const proxy=new Proxy(timi,{});
  3. console.log(proxy istance of Date); //返回true
  4. proxy.getDate(); //TypeError

9.2 代理捕获器与反射方法

代理可以捕获13种不同的操作方法,这些操作有各自不同的反射API方法。参数,关联ECMAScript操作和不变式

9.2.1 get()

get()捕获器会在获取属性值的操作中被调用。对应的反射API方法为Reflect.get()

  1. let person={
  2. name:"JackMa",
  3. }
  4. let proxy=new Proxy(person,{
  5. get(tar,prop,rec){
  6. console.log(tar);
  7. console.log(prop);
  8. console.log(receiver);
  9. console.log(tar[prop]);
  10. return Reflect.get(...arguments);
  11. }
  12. });
  13. 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()

  1. let person={
  2. name="Timi"
  3. }
  4. let proxy=new Proxy(person,{
  5. set(target,property,value,receiver){
  6. console.log('set()');
  7. return Reflect.set(...arguments);
  8. }
  9. });
  10. proxy.name="JackMa"; //返回 set()
  11. 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()

  1. let person={
  2. name:'JackMa',
  3. }
  4. let proxy=new Proxy(person,{
  5. has(target,property){
  6. console.log('Has');
  7. return Reflect.has(...arguments);
  8. }
  9. });
  10. console.log('JackMa' in proxy); //返回false
  11. console.log('name' in proxy); //返回true

9.2.4defineProperty()

defineProperty()捕获器会在Object.defineProperty()中被调用。对应的反射API方法为Reflect.defineProperty()

  1. let person={
  2. };
  3. const proxy=new Proxy(person,{
  4. defineProperty(target,property,descriptor){
  5. console.log('defineProperty()');
  6. return Reflect.defineProperty(...arguments)
  7. }
  8. });
  9. Object.defineProperty(proxy,'name',{value:'JackMa'}); //返回true
  10. console.log(proxy.name); //返回Jack Ma

9.2.5getOwnPropertyDescriptor()

9.3代理模式(暂时略过)

使用代理可以在代码中实现一些有用的编程模式

9.3.1 跟踪属性访问

通过捕获get、set和has等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获器的某个对象代理放到应用中,可以监控这个对象合适再何处被访问