Featured image of post JavaScript 代理和 Reflect API:元编程模式Featured image of post JavaScript 代理和 Reflect API:元编程模式

JavaScript 代理和 Reflect API:元编程模式

探索用于元编程的 JavaScript 代理和 Reflect API。学习 Vue 3 和 MobX 中的 13 个陷阱、Reflect 方法、验证、虚拟属性和现实世界模式。

JavaScript 的 ProxyReflect API 是该语言中最强大的元编程工具之一。它们允许开发人员拦截和自定义对象的基本操作——属性访问、赋值、枚举、函数调用,甚至构造函数调用。本文探讨了所有 13 个代理陷阱、互补的 Reflect API 以及 Vue 3、MobX 和 Immer 等生产框架中使用的真实模式。

了解代理模式

Proxy 包装目标对象并通过称为陷阱的处理函数拦截操作:

const target = { name: "Alice", age: 30 };
const handler = {
  get(target, prop, receiver) {
    console.log(`Getting property "${prop}"`);
    return Reflect.get(target, prop, receiver);
  }
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // Logs: Getting property "name" → "Alice"

目标保持不变。处理程序定义如何转发、修改或阻止代理上的操作。代理与包装函数或装饰器不同,因为它在语言级别进行拦截 - indeletefor...innew 都可以自定义。


13 个代理陷阱

Proxy 提供了 13 个陷阱,涵盖了每个基本的对象操作。最常用的是getset,它们拦截属性的读写。这是一个完整的参考:

陷阱何时触发论点
占位符_0物业阅读占位符_1
占位符_0属性写占位符_1
占位符_0in 运算符占位符_2
占位符_0delete 运算符占位符_2
占位符_0Object.keys()for...in占位符_3
占位符_0函数调用占位符_1
占位符_0new 关键字占位符_2
占位符_0占位符_1占位符_2
占位符_0占位符_1占位符_2
占位符_0占位符_1占位符_2
占位符_0占位符_1占位符_2
占位符_0占位符_1占位符_2
占位符_0占位符_1占位符_2

每个陷阱都必须强制执行不变量。例如,get 陷阱不能返回与不可写、不可配置属性的实际值不同的值。


反射 API

Reflect 方法镜像每个代理陷阱并提供默认转发行为。在陷阱内使用 Reflect 可确保 receiver 参数的正确传播,这对于继承属性和 getter 中正确的 this 绑定至关重要:

const parent = {
  get fullName() { return `${this.first} ${this.last}`; }
};
const handler = {
  get(target, prop, receiver) {
    // Without Reflect.get, the getter's 'this' would be wrong
    return Reflect.get(target, prop, receiver);
  }
};
const proxy = new Proxy(Object.create(parent), handler);
proxy.first = "John";
proxy.last = "Doe";
console.log(proxy.fullName); // "John Doe"

思维模型很简单:ReflectProxy 的关系就像 Object 与直接属性访问一样。始终通过 Reflect 转发陷阱,以在添加自定义逻辑时保持默认行为。


验证和数据完整性

set 陷阱可以对对象强制实施架构约束:

const validator = {
  set(target, prop, value) {
    if (prop === "age" && (!Number.isInteger(value) || value < 0)) {
      throw new TypeError("Age must be a positive integer");
    }
    if (prop === "email" && !/^[^\s@]+@[^\s@]+$/.test(value)) {
      throw new TypeError("Invalid email format");
    }
    return Reflect.set(target, prop, value);
  }
};
const user = new Proxy({}, validator);
user.age = 25;  // OK
user.age = -5;  // TypeError: Age must be a positive integer

只读代理会阻止所有突变陷阱:

const readOnly = target =>
  new Proxy(target, {
    set: () => { throw new Error("Object is read-only"); },
    deleteProperty: () => { throw new Error("Object is read-only"); },
    defineProperty: () => { throw new Error("Object is read-only"); },
  });

虚拟属性和计算值

Proxy 可以通过在 get 陷阱中动态计算目标对象上不存在的属性来公开这些属性:

const range = (from, to) =>
  new Proxy({ from, to }, {
    get(target, prop) {
      if (prop === "size") return target.to - target.from + 1;
      if (prop === "contains") return (v) => v >= target.from && v <= target.to;
      return Reflect.get(target, prop);
    }
  });

const r = range(1, 10);
console.log(r.size);      // 10
console.log(r.contains(5)); // true

这种惰性求值模式可以通过 WeakMap 进行缓存扩展,以避免重新计算。


财产观察和变化跟踪

使用 set 陷阱的简单可观察:

function observe(target, onChange) {
  return new Proxy(target, {
    set(target, prop, value, receiver) {
      const result = Reflect.set(target, prop, value, receiver);
      onChange(prop, value);
      return result;
    }
  });
}

为了进行深度观察,请使用 WeakMap 缓存递归地包装嵌套对象,以避免双重包装。该技术是 Vue 3 的 reactive() 系统的基础,它取代了 Vue 2 的基于 Object.defineProperty 的方法。


可撤销代理

Proxy.revocable 创建一个可以永久禁用的代理:

const { proxy, revoke } = Proxy.revocable(target, handler);
// use proxy normally...
revoke();
// any subsequent operation throws TypeError

这对于授予对敏感资源的临时访问权限和执行清理合同非常有用。


生产模式:Vue 3、MobX、Immer

Vue 3 在其 reactive() 系统中使用 Proxy。 get 陷阱跟踪属性访问以构建依赖关系图,而 set 触发重新渲染。 MobX 6+ 同样将 defineProperty 替换为 observable() 的代理。 Immer.js 使用 Proxy 生成具有写时复制语义的不可变草稿 — set 标记修改,而 get 在访问时创建嵌套草稿。


性能考虑因素

每个被捕获的操作都会产生开销。避免在紧密循环中代理热路径对象。有选择地应用代理——“代理内部,而不是外部”——并在优化之前进行分析。具有大型 WeakMap 缓存的深度代理也会影响内存使用。

代理是验证、反应性和虚拟资源的正确工具。对于性能关键路径,请考虑更简单的模式是否足够。如果应用得当,Proxy 可以实现任何其他 JavaScript 功能不可能或繁琐的元编程模式。