JavaScript 的元编程能力长期依赖 Object.defineProperty 或者 eval 等手段,这类方案要么只能处理固定属性,要么在语法层面极不透明。Proxy 的出现改变了这一局面:它允许开发者为一个普通对象创建可编程的代理层,将原本由引擎内部完成的属性查找、赋值、删除等操作暴露给 JavaScript 代码。配合 Reflect 提供的默认行为映射,拦截逻辑可以做到安全且可复用。

Proxy 到底拦截了什么
Proxy 构造函数接收两个参数:目标对象 target 和处理对象 handler。handler 中可以定义一系列陷阱函数,每个陷阱都对应一种语言内部操作。例如读取属性的内部方法 [[Get]] 会触发 get 陷阱,写入属性触发 set 陷阱,删除属性触发 deleteProperty 陷阱,查询属性是否存在则触发 has 陷阱。总共有十三种陷阱,覆盖了对象操作的大部分入口。理解这一点是掌握元编程拦截的基础,因为只有真正从代理对象发起的操作才会被拦截,直接操作原始目标对象不会触发任何陷阱。
以下代码展示了一个最基础的双向拦截:读取 name 属性时记录日志,写入 age 属性时强制转换为数字。
const target = { name: 'Alice', age: 25 };
const handler = {
get(obj, prop) {
console.log(`读取属性: ${String(prop)}`);
return obj[prop];
},
set(obj, prop, value) {
if (prop === 'age') {
value = Number(value);
if (Number.isNaN(value)) {
throw new TypeError('age 必须是数字');
}
}
console.log(`写入属性: ${String(prop)} = ${value}`);
obj[prop] = value;
return true;
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // 先打印读取日志,再输出 Alice
proxy.age = '30'; // 自动转换为数字 30
console.log(target.age); // 30
注意 set 陷阱必须返回布尔值,返回 false 在严格模式下会抛出 TypeError。如果目标对象上的某个属性是不可写且不可配置的,那么 get 和 set 的拦截结果必须与目标属性保持一致,否则引擎会直接抛出 TypeError。这种约束保证了代理行为不会破坏语言的基本不变量。
Reflect 为什么是 Proxy 的最佳搭档
Reflect 是 ES6 引入的内置对象,它提供了与 Proxy 陷阱一一对应的静态方法:Reflect.get、Reflect.set、Reflect.has、Reflect.deleteProperty 等。这些方法做的事情和语言内部操作完全一致,但以普通函数形式暴露出来。在代理陷阱中直接操作 target 往往容易忽略原型链上的 getter 或者依赖异常返回值的场景,而 Reflect 可以返回操作是否成功,避免手动 try catch。
下面是一个对比例子:如果不用 Reflect,手写 get 陷阱时直接返回 target[prop],当目标属性来自原型链且 getter 依赖 this 时,this 会错误指向 target 而不是代理对象。使用 Reflect.get 并传入 receiver 参数可以修正这一点。
const proto = {
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
};
const target = Object.create(proto);
target.firstName = 'Ada';
target.lastName = 'Lovelace';
const handler = {
get(obj, prop, receiver) {
// 使用 Reflect.get 将 receiver 透传给原型链上的 getter
return Reflect.get(obj, prop, receiver);
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.fullName); // Ada Lovelace
receiver 参数在多层代理和继承场景中尤其重要。如果只写 obj[prop],原型链上的 getter 内部的 this 会指向 obj(即 raw target),此时访问 this.firstName 可能并不经过代理的 get 陷阱,导致拦截失效。Reflect 方法把 receiver 作为 this 上下文传递进去,保持了访问路径的一致性。类似地,set 陷阱中通过 Reflect.set 可以拿到布尔返回值,避免手动返回 true 掩盖失败情况。
用 Proxy 实现数据校验与只读对象
数据校验是 Proxy 最直观的应用之一。在表单对象或配置对象上,往往需要保证某些字段类型正确、取值范围合法。常规做法是在每个修改入口添加 if 判断,但很容易遗漏。把校验逻辑集中到 set 陷阱中,可以让所有赋值操作统一经过同一道闸门。
下面的示例创建了一个只读代理。任何对深层属性的写入、删除或重新定义都会失败,且错误信息明确指向被拦截的属性。
function createReadonly(obj) {
return new Proxy(obj, {
set(target, prop) {
throw new TypeError(`属性 ${String(prop)} 为只读,不可赋值`);
},
deleteProperty(target, prop) {
throw new TypeError(`属性 ${String(prop)} 为只读,不可删除`);
},
defineProperty(target, prop) {
throw new TypeError(`属性 ${String(prop)} 为只读,不可重新定义`);
}
});
}
const config = createReadonly({ apiKey: 'abc123', retries: 3 });
config.retries = 5; // TypeError: 属性 retries 为只读,不可赋值
这个例子同时展示了 deleteProperty 和 defineProperty 陷阱的必要性。如果只拦截 set,仍然可以通过 Object.defineProperty 修改属性描述符,或者通过 delete 删除属性。真正健壮的只读实现需要拦截所有可以改变对象结构的内部操作。另一个常见需求是类型校验,可以在 set 中根据已有值类型做宽松检查,或使用 TypeScript 之外的方式在运行时兜底。
简易响应式系统与常见陷阱
现代前端框架的响应式能力很多都基于 Proxy 构建。依赖收集的基本思路是:在 get 陷阱中记录当前正在执行的副作用函数,在 set 陷阱中触发这些依赖。这样当数据变化时,相关的更新函数会自动运行。下面实现一个最小可运行的响应式模型。
let activeEffect = null;
const depsMap = new Map();
function reactive(target) {
return new Proxy(target, {
get(obj, prop, receiver) {
if (activeEffect) {
let effects = depsMap.get(prop);
if (!effects) {
effects = new Set();
depsMap.set(prop, effects);
}
effects.add(activeEffect);
}
return Reflect.get(obj, prop, receiver);
},
set(obj, prop, value, receiver) {
const result = Reflect.set(obj, prop, value, receiver);
const effects = depsMap.get(prop);
if (effects) {
effects.forEach(fn => fn());
}
return result;
}
});
}
const state = reactive({ count: 0 });
function effect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}
effect(() => console.log('count 现在是', state.count));
state.count = 1; // 自动打印 count 现在是 1
这段代码有一个关键点:使用 Reflect.get 时传入了 receiver,否则依赖收集会绕过代理对象。另一个隐藏陷阱是 this 绑定。如果原始对象的方法内部通过 this 访问自身属性,而该对象又通过 Proxy 包装后调用方法,this 将指向代理对象,这通常正是响应式系统期望的行为。但如果将原始方法单独取出调用,this 就会丢失,代理拦截也会失效。
性能方面,每一次属性访问都要经过一层函数调用和可能的日志、校验逻辑,在高频场景中可能成为瓶颈。此外,Proxy 不能完整模拟某些内置对象的行为,例如直接代理 Map 或 Set 时,调用内部槽方法可能报出不兼容错误,需要额外绑定原始方法。兼容性上现代浏览器和 Node.js 已全面支持 Proxy,但无法通过 polyfill 完整模拟,因为拦截能力本身就是语法层的。
JavaScript ProxyReflect元编程修改时间:2026-10-06 23:20:08