Proxy的set拦截器为什么在修改嵌套对象属性时没触发?
我在用Proxy做表单验证时遇到奇怪的问题,给对象设置了set拦截器,修改顶层属性能正常触发,但修改嵌套对象的属性却完全没反应。比如这样写:
const form = { user: { name: '' } };
const proxy = new Proxy(form, {
set(target, key, value) {
console.log('修改了', key); // 只在修改form的顶层属性时输出
return Reflect.set(...arguments);
}
});
proxy.user.name = '测试'; // 这里完全没有触发拦截器
我试过把整个对象结构都转成Proxy,但这样嵌套三层以上就会报最大调用栈错误。难道Proxy不能直接拦截嵌套对象的修改吗?应该怎么正确监听深层属性的变化?
form创建了一个 Proxy,但它只能拦截对form自身的操作,比如设置form.user或form.age。但当你操作的是form.user.name时,实际修改的是user对象的属性,而不是form对象,所以 Proxy 的 set 拦截器不会触发。要监听嵌套对象的属性变化,你有几种选择。下面我来一步步说明怎么做。
第一种方案是递归地为每个对象创建 Proxy。你提到嵌套三层会报最大调用栈错误,那可能是因为你在递归创建 Proxy 时没有处理好循环引用。我们可以加个判断,避免循环引用的问题。
这里是一个经过处理的版本,可以安全地为嵌套对象创建 Proxy:
function createDeepProxy(obj, handler) {
// 如果不是对象或为 null,直接返回
if (typeof obj !== 'object' || obj === null) return obj;
// 创建 Proxy
const proxy = new Proxy(obj, {
...handler,
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver);
// 如果取出来的值是对象,就递归创建 Proxy
if (typeof value === 'object' && value !== null) {
return createDeepProxy(value, handler);
}
return value;
}
});
return proxy;
}
然后你就可以这样使用:
const form = { user: { name: '' } };
const proxy = createDeepProxy(form, {
set(target, key, value) {
console.log('修改了', key);
return Reflect.set(target, key, value);
}
});
proxy.user.name = '测试'; // 这次会输出 "修改了 name"
这个方案的关键在于每次 get 的时候,如果发现值是对象,就再包一层 Proxy。这样无论访问多深的嵌套属性,都能保证每个层级的对象都被代理。
不过这种方式也有一些缺点:
1. 首次读取属性时才创建 Proxy,性能上会有一点开销
2. 如果对象结构很大,递归代理可能会变慢
3. 一些动态新增的属性不会自动被代理,除非你手动重新包裹
如果你需要监听 Vue 或 React 这种大规模嵌套结构的变化,建议参考它们的响应式系统实现,比如 Vue 使用了 Object.defineProperty 或 Proxy + Reflect 的组合,并配合依赖收集机制。
如果你希望更轻量一点,也可以考虑只在需要监听的层级上创建 Proxy。比如你知道
form.user是一个对象,你可以手动为它再创建一个 Proxy:const form = {
user: new Proxy({ name: '' }, {
set(target, key, value) {
console.log('user 修改了', key);
return Reflect.set(target, key, value);
}
})
};
const proxy = new Proxy(form, {
set(target, key, value) {
console.log('form 修改了', key);
return Reflect.set(target, key, value);
}
});
proxy.user.name = '测试'; // 会输出 "user 修改了 name"
这种方法适用于对象结构比较固定的情况,写起来也比较清晰。
总结一下:
- Proxy 只能代理当前对象,不能自动代理嵌套对象
- 如果想监听深层属性,需要递归创建 Proxy
- 注意处理循环引用和性能问题
- 如果结构固定,也可以手动包裹嵌套对象
你可以根据自己的需求选择合适的方式。