Kodokon kodokon.com

Proxy、Reflect 与元编程

用 Proxy 陷阱拦截语言的基本操作,并用 Reflect 干净地委托默认行为。

10 分钟 · 3 题

在 Kodokon 中打开本课

Proxy 会包裹一个目标对象,并通过十三个陷阱拦截语言的内部操作getsethasin 运算符)、deletePropertyownKeysapplyconstruct 等等。Reflect 对象以函数的形式暴露了完全相同的这些操作,其签名与这些陷阱一致。它是 Vue 3 响应式系统、动态 mock 以及 ORM 风格 API 的基础。

JAVASCRIPT
const target = { name: "Ada" };

const traced = new Proxy(target, {
  get(obj, prop, receiver) {
    console.log("get " + String(prop));
    return Reflect.get(obj, prop, receiver);
  }
});

traced.name; // get name
一个委托给 Reflect 的最小 get 陷阱。

为什么用 Reflect.get(obj, prop, receiver) 而不是 obj[prop]?因为接收者(receiver)。如果该属性是一个访问器(getter),Reflect.get 会以 this 指向接收者——也就是代理本身或某个继承自它的对象——来调用它,而 obj[prop] 会把 this 设为原始目标。如果没有接收者,一旦某个对象继承自代理,读取 this 的 getter 就会失效。

JAVASCRIPT
const base = {
  get label() {
    return this.name;
  }
};

const proxy = new Proxy(base, {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver);
  }
});

const child = Object.create(proxy);
child.name = "Ada";
console.log(child.label); // "Ada" thanks to the receiver
接收者在被继承的 getter 中保留了 this。

真正严肃的用例:写入时的校验、即时计算的虚拟属性、日志记录、访问控制(通过 hasownKeys 隐藏以 _ 为前缀的键),或者带有默认行为的对象。请注意,Reflect.set 在严格模式下返回一个布尔值而不是抛出异常:你的陷阱必须把这个返回值传播出去

JAVASCRIPT
function validated(target) {
  return new Proxy(target, {
    set(obj, prop, value, receiver) {
      if (prop === "age" && !Number.isInteger(value)) {
        throw new TypeError("age must be an integer");
      }
      return Reflect.set(obj, prop, value, receiver);
    }
  });
}

const user = validated({ age: 30 });
user.age = 31;       // ok
// user.age = "old"; // TypeError
写入时的透明校验。

知识检测

确认你已牢记本课的重点内容。

  1. get 陷阱中,为什么要优先使用 Reflect.get(target, prop, receiver) 而不是 target[prop]
    • 因为 Reflect 绕过原型,所以更快
    • 这样 getter 就能以正确的 this(接收者)运行,而不是以原始目标运行
    • 因为 target[prop] 会重新触发陷阱并导致无限递归
    • 这两种写法严格等价
  2. 哪个陷阱会拦截表达式 "id" in obj
    • get
    • has
    • ownKeys
    • getOwnPropertyDescriptor
  3. 一个 get 陷阱对某个目标属性返回 42,而该属性的值为 1,被声明为不可配置且不可写。会发生什么?
    • 代理返回 42,陷阱始终优先
    • 引擎静默地返回 1
    • 引擎因不变量被违反而抛出 TypeError
    • 行为取决于 JavaScript 引擎