JavaScript 的 Proxy 和 Reflect 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"
目标保持不变。处理程序定义如何转发、修改或阻止代理上的操作。代理与包装函数或装饰器不同,因为它在语言级别进行拦截 - in、delete、for...in 和 new 都可以自定义。
13 个代理陷阱
Proxy 提供了 13 个陷阱,涵盖了每个基本的对象操作。最常用的是get和set,它们拦截属性的读写。这是一个完整的参考:
| 陷阱 | 何时触发 | 论点 |
|---|---|---|
| 占位符_0 | 物业阅读 | 占位符_1 |
| 占位符_0 | 属性写 | 占位符_1 |
| 占位符_0 | in 运算符 | 占位符_2 |
| 占位符_0 | delete 运算符 | 占位符_2 |
| 占位符_0 | Object.keys()、for...in | 占位符_3 |
| 占位符_0 | 函数调用 | 占位符_1 |
| 占位符_0 | new 关键字 | 占位符_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"
思维模型很简单:Reflect 与 Proxy 的关系就像 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 功能不可能或繁琐的元编程模式。

