JS中的Proxy和Reflect

ES6 新增的 Proxy 和 Reflect 提供了拦截能力和向基本操作中嵌入额外行为的能力。此前的 ES6 并没有类似代理的特性, 因此,Proxy 是一种新的基础性语言能力,无法 polyfill

基础使用

const target = { id: 1 };
const handler = {};
const proxy = new Proxy(target, handler);

console.log(proxy.id); // 1

// 给属性目标赋值会反应在两个对象上
target.id = 2;
console.log(proxy.id); // 2

// Proxy.prototype 是 undefined, 因此不能用instanceof 操作符
console.log(proxy instanceof Proxy); // TypeError: Function has non-object-prototype

捕获器

使用代理的主要目的是可以定义捕获器,用来处理一些基本操作。每次在代理对象上调用这些基本操作时, 代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。 所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为。 比如,get()捕获器会接收到目标对象、要查询的属性和代理对象三个参数。

const target = { id: 1 };
const handler = {
  get() { return 'from proxy get' }
};
const proxy = new Proxy(target, handler);

console.log(target.id); // from proxy get

捕获器不变式

根据 ECMAScript 规范,每个捕获的方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵循“捕获器不变式”(trap invariant)。捕获器不变式因方法不同而异,但通常都会防止捕获器定义出现过于反常的行为。 比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出 TypeError:

const target = {};
Object.defineProperty(target, 'id', {
  configurable: false,
  writable: false,
  value: 1,
});
const handler = {
  get() {
    return 2;
  },
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // TypeError

撤销代理

有时候可能需要中断代理对象与目标对象之间的联系。new Proxy 创建的代理在生命周期种会一直存在。 Proxy 也暴露了 revocable()方法,这个方法支持撤销代理对象与目标对象的关联。后续可直接调用撤销函数 revoke() 来撤销代理。 撤销代理之后再调用代理会抛出 TypeError,撤销函数和代理对象是在实例化时同时生成的:

const target = { id: 1};
const handler = {
  get() {
    return 2;
  },
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.foo); // 2
console.log(target.foo); // 1
revoke();
console.log(proxy.foo); // TypeError

状态标记

const target = {};
try {
  Object.defineProperty(target, id, 1);
  console.log('success');
} catch (e) {
  console.log('failed')
}

如果定义新属性发生异常,Reflect.defineProperty 会返回false,而不是抛异常,因此可以如此重构

const target = {};
if (Reflect.defineProperty(target, id, 1)) {
  console.log('success');
} else {
  console.log('failed')
}

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

以下反射方法是只有操作符才能完成的操作

代理的问题和不足

代理中的this

const wm = new WeakMap();
class User {
  constructor(id) {
    wm.set(this, id);
  }
  set id(id) {
    wm.set(this, id)
  }
  get id() {
    return wm.get(this);
  }
}
// 由于这个实现依赖 User 实例的对象标识,在这个实例被代理的情况就会出问题
const user = new User(123);
const proxy = new Proxy(user, {});
console.log(proxy.id); // undefined

这是因为 User 实例一开始使用目标对象作为 WeakMap 的键,代理对象却常是从自身取得这个实例。要解决这个问题需要重新配置代理, 把代理User实例改为代理User类本身。之后再创建代理的实例就会以代理实例作为WeakMap的键了。

const UserProxy = new Proxy(User, {});
const proxy = new UserProxy(123);
console.log(proxy.id);

代理与内部槽位

代理与内置引用类型(比如Array)的实例通常可以很好地协同,但有些 ECMAScript内置类会依赖代理无法控制的机制, 结果导致在代理上调用某些方法会出错。一个典型的例子就是 Date类型。根据 ECMAScript规范, Date 类型方法的执行依赖 this值部槽位[[NumberDate]]。代理对象上不存在这个内部槽位, 而且这个内部槽位的值也不能通过get()和set()操作访问到,于是代理拦截后本应转发给目标对象的方法会抛出 TypeError:

const date = new Date();
const proxy = new Proxy(date, {});
console.log(proxy instanceof date); // true
proxy.getDate(); // TypeError: 'this' is no a Date object

其它

在Vue2到Vue3的演进中,Vue3放弃了Object.defineProperty,改用了Proxy和Reflect,有以下原因:

Proxy 的优点有: