动态深层代理
约 1417 字大约 5 分钟
2025-11-22
普通 Proxy 对嵌套对象的拦截 “天生失效”,而 “懒代理(动态深层代理)” 正是解决这个问题的关键方案,本质就是 “访问到嵌套子对象时,才为其创建代理”。
普通 Proxy 拦截不到嵌套对象
Proxy 的工作机制是 “代理目标对象本身”,而不是递归代理其属性值。对于嵌套对象,它的属性值是 “另一个对象的引用”,普通 Proxy 只会拦截 “获取这个引用” 的操作(比如 obj.nested),但不会拦截 “通过这个引用访问子属性” 的操作(比如 obj.nested.key)。
// const target = {
// name: "顶层属性",
// nested: { // 嵌套对象
// age: 18
// }
// };
// 本质上等价于:
const nested = {
age: 18
};
const target = {
name: "顶层属性",
nested // 属性简写写法,等价于 nested: nested
};
// 普通 Proxy:只代理 target 本身
const proxy = new Proxy(target, {
get(target, key) {
console.log(`拦截到:访问顶层属性 ${key}`);
return target[key]; // 直接返回嵌套对象的引用,没有对其代理
},
set(target, key, value) {
console.log(`拦截到:修改顶层属性 ${key} = ${value}`);
target[key] = value;
return true;
}
});
// 测试:
proxy.name; // 触发拦截 → 正确
proxy.nested; // 触发拦截(获取 nested 引用)→ 正确
proxy.nested.age; // 不触发拦截!→ 因为访问的是“nested 引用指向的对象”的 age 属性
proxy.nested.age = 20; // 不触发拦截!→ 同理嵌套对象的 age 属性读写完全没被拦截,这就是普通 Proxy 的 “局限性”。
懒代理(动态深层代理)
“懒代理” 的核心思想是延迟代理:不为嵌套对象提前创建代理(避免浪费性能),而是在 “首次访问该嵌套对象” 时,动态为其创建 Proxy 并替换原引用,后续对子对象的所有操作,就都能被新 Proxy 拦截了。
关键步骤拆解:
- 代理顶层对象时,在
get拦截器中判断:如果访问的属性值是 “对象 / 数组”(且未被代理过),就为其创建子代理; - 将子代理对象替换原属性值(比如
target.nested = 子代理),确保后续再访问obj.nested时,拿到的是代理后的对象; - 子代理的拦截逻辑可以和顶层代理一致(也可自定义),形成 “递归代理” 效果,但触发时机是 “访问时”(懒加载)。
实现一个简单的懒代理
改造普通 Proxy,实现懒代理:
// 工具函数:判断是否为需要代理的对象(排除 null、基础类型)
function isObject(value) {
return typeof value === "object" && value !== null;
}
// 懒代理工厂函数:递归创建动态深层代理
function createLazyDeepProxy(target, handler = {}) {
// 存储“已代理的子对象”(避免重复代理同一对象)
const proxyCache = new WeakMap();
// 核心:创建代理的函数(可递归调用)
function createProxy(target) {
// 如果已经代理过,直接返回缓存的代理对象
if (proxyCache.has(target)) {
return proxyCache.get(target);
}
// 自定义默认拦截器(可被外部 handler 覆盖)
const defaultHandler = {
get(target, key) {
const value = Reflect.get(target, key); // 用 Reflect 确保行为和原生一致
console.log(`拦截到:访问 ${key}(${isObject(value) ? "嵌套对象" : "普通属性"})`);
// 关键:如果访问的是对象/数组,且未代理过,动态创建子代理
if (isObject(value)) {
const subProxy = createProxy(value);
// 替换原属性值为子代理(下次访问直接拿到代理,无需重复创建)
Reflect.set(target, key, subProxy);
return subProxy;
}
return value;
},
set(target, key, value) {
console.log(`拦截到:修改 ${key} = ${value}`);
// 如果新值是对象,先为其创建代理再赋值
if (isObject(value)) {
value = createProxy(value);
}
return Reflect.set(target, key, value);
}
};
// 合并外部传入的 handler 和默认 handler(外部优先级更高)
const proxyHandler = { ...defaultHandler, ...handler };
const proxy = new Proxy(target, proxyHandler);
// 缓存代理对象,避免重复代理
proxyCache.set(target, proxy);
return proxy;
}
return createProxy(target);
}测试懒代理效果:
const target = {
name: "顶层属性",
nested: { age: 18 }, // 嵌套对象
list: [1, 2, 3] // 嵌套数组(也能被代理)
};
const lazyProxy = createLazyDeepProxy(target);
// 测试访问:
lazyProxy.name; // 拦截到:访问 name(普通属性)→ 顶层属性
lazyProxy.nested; // 拦截到:访问 nested(嵌套对象)→ 动态创建子代理
lazyProxy.nested.age; // 拦截到:访问 age(普通属性)→ 子代理生效
lazyProxy.list[0]; // 拦截到:访问 0(普通属性)→ 数组元素也能拦截
// 测试修改:
lazyProxy.nested.age = 20; // 拦截到:修改 age = 20 → 子代理拦截
lazyProxy.list.push(4); // 拦截到:访问 push(嵌套对象)→ 数组的方法也能拦截懒代理的核心特点
懒加载:嵌套对象只有在 “首次被访问” 时才会创建代理,而不是初始化时就递归代理所有层级,避免了对大型嵌套对象的性能浪费(比如嵌套 10 层,但只访问前 3 层,后 7 层不会被代理)。
动态代理:访问子对象时才 “动态生成代理”,并替换原引用,后续所有操作都基于代理对象。
递归生效:不仅支持对象嵌套,还支持数组、甚至对象里嵌套数组的复杂结构(上面的示例已验证)。
缓存避免重复代理:用 WeakMap 缓存已代理的对象,防止同一对象被多次代理(比如 obj.a = obj.b 这种循
补充
预代理:初始化时就递归遍历所有嵌套层级,为每个对象都创建代理,优点是简单,缺点是性能差(大型对象初始化慢)、浪费内存(未访问的子对象也被代理)。
懒代理:访问时才代理,优点是性能优(按需代理)、内存占用低,缺点是需要处理 “代理缓存” 和 “引用替换”,实现稍复杂。