JavaScript红宝书06-代理和反射(Proxies and Reflect)

67 阅读4分钟

代理基础

代理行为作为一种目标对象的抽象,在许多方面都与C++中指针作为代理执行目标对象类似,目标对象可以直接操作或者通过代理,但是直接操作会绕过代理操作。

创建透传代理

在最简单的形式下,代理仅仅可以作为一个抽象的目标对象存在,默认所有的操作通过代理对象,显式传给目标对象。因此,可以在相同位置和方式使用一个与目标对象关联的代理对象。

创建代理对象,通过代理构造器,需要两个参数,目标对象和操作方法,操作方法可以修改目标对象的相关信息,完成通过代理修改对象如下。

const target = {
  id: 'target'
};

const handler = {
  get() {
    return target.id + '666'
  }
};

const proxy = new Proxy(target, handler);
console.log(proxy.id);

定义拦截器

使用代理对象的主要目的,就是可以定义拦截器,用于拦截基本操作,每个处理器(handler)都有零个、一个或多个拦截器,每个拦截器都可以被代理对象直接或间接的调用,在唤醒目标对象前,代理会先唤醒拦截器,允许中断并且修改目标对象的行为。就比如上面的拦截get操作。

拦截器参数

Reflect API

Reflect 允许修改拦截器方法用最少的代码。每个拦截方法要注意目标对象的上下文和拦截方法签名,并且拦截器的处理方法必须遵守“拦截不变性”原则。

可撤销的代理

Proxy暴露了一个revocable()方法,提供了一个撤销方法,调用之后可以把代理对象和目标对象解除关联,除此之外,撤销方法是幂等的,调用多少次效果都是一样的。代理撤销后再调用相关的方法都会抛出TypeError错误。

Reflect API

Reflect API没有限制拦截器

Reflect API 方法都会有类似对象类型

状态标志

多数Reflect方法会返回一个布尔值,如果操作成功返回true,在确定情况下会比其他的Reflect API更好用,直接修改对象或者抛出

一级函数代替运算符

Reflect.get()等价于访问操作

Reflect.set()等价于赋值操作

Reflect.has()等价于with()方法

Reflect.deleteProperty()等价于删除操作

Reflect.construct()等价于new操作

代理的考虑和缺点

代理是在ECMAScript中新增的基础建设,并且尽最大的努力实现,最主要部分是作为对象的虚拟层,但无论如何,代理也不能永远ECMAScript保持无缝结构。

代理中使用this

WeakMap创建的的对象,代理中使用this会出现undefined问题

const user = new User(123);
console.log(user.id); 

const userInstanceProxy = new Proxy(user, {});
console.log(userInstanceProxy.id); 

出现这种情况是因为,user实例是WeakMap创建,且作为目标对象,但是代理尝试通过代理对象检索实例,解决方法就是重新配置代理对象初始化时作为key插入到代理的实例中

代理和内部插槽

通常情况下,内部引用类型实例可以与代理无缝工作(例如数组),但是,一些内部类型依赖代理不可控机制。这样做的结果是,包装的实例不能正常的工作。

比较有代表的例子是Date类型,当执行方法时,Date类型依赖存在名为[[NumberData]]内部插槽上的值,因为内部插槽不存在代理,并且这些插槽的值不允许一般的getter和setter进行访问,代理一但中断或者重新指向target就会抛出TypeError。

const target = new Date();
const proxy = new Proxy(target, {});

console.log(proxy instanceof Date); // true

proxy.getDate(); 

代理拦截和反射方法

代理可以拦截13种不同的基础操作,每一个反射API都拥有自己的参数、ECMAScript的相关操作和不变性。

代理的get()拦截方法与Reflect.get()方法类似

代理的set()拦截方法与Reflect.set()方法类似

属性验证:由于所有的值的分配都需要经过set()方法的拦截,此时可以允许或者拒绝值的分配

方法和构造函数的参数也可以进行验证,同样可以选择拒绝