代理模式
为一个对象提供一个替身,以便控制对它的访问。
代理与本体实现相同的接口,因此对使用方完全透明——使用方以为自己在直接操作本体。
Client → Proxy → RealSubject
(拦截、增强、延迟、缓存、鉴权)
一、虚拟代理:把昂贵的操作推迟到真正需要时
最经典的例子是图片预加载:先展示一张占位图,等真图加载完再替换。
// 本体:只负责把 src 设置到 DOM 上,职责单一
const myImage = (function () {
const imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc(src) { imgNode.src = src; }
};
})();
// 代理:负责预加载这件"额外"的事
const proxyImage = (function () {
const img = new Image();
img.onload = function () {
myImage.setSrc(this.src); // 真图加载完成后再替换
};
img.onerror = function () {
myImage.setSrc('/fallback.png'); // 失败兜底
};
return {
setSrc(src) {
myImage.setSrc('/loading.gif'); // 先占位
img.src = src;
}
};
})();
proxyImage.setSrc('https://example.com/big.jpg');
这里体现的是单一职责
myImage 只做"设置 src",proxyImage 只做"预加载"。哪天不需要预加载了(比如换成原生 loading="lazy"),直接把 proxyImage 换回 myImage 即可,本体一行都不用改。
同类场景:图片/组件懒加载(IntersectionObserver)、路由级代码分割(() => import('./Page.vue'))、大列表虚拟滚动。
二、缓存代理:用空间换时间
function createCacheProxy(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const expensiveCalc = (a, b) => { /* 假设很慢 */ return a * b; };
const cachedCalc = createCacheProxy(expensiveCalc);
cachedCalc(2, 3); // 计算
cachedCalc(2, 3); // 命中缓存
生产可用的缓存代理需要考虑四件事
- key 的生成:
JSON.stringify对参数顺序敏感({a:1,b:2}与{b:2,a:1}不同),且无法处理函数、undefined、循环引用。复杂场景要自定义resolver。 - 缓存淘汰:
Map会无限增长导致内存泄漏,需要 LRU 或 TTL。只以对象为 key 且允许被回收时,用WeakMap。 - 异步与失败:缓存的是 Promise 的话,rejected 的 Promise 必须从缓存中删掉,否则一次网络抖动会被永久缓存。
- 失效时机:数据变更后如何清除缓存,这通常比缓存本身更难。
请求去重就是缓存代理的一个变种:
const pending = new Map();
function dedupeRequest(url) {
if (pending.has(url)) return pending.get(url); // 并发时复用同一个 Promise
const p = fetch(url)
.then(r => r.json())
.finally(() => pending.delete(url)); // 完成后移除,下次重新请求
pending.set(url, p);
return p;
}
三、保护代理:访问控制
在真正调用前做权限、参数、频率校验:
function withAuth(target, currentUser) {
return new Proxy(target, {
get(obj, prop, receiver) {
if (prop.startsWith('_')) throw new Error('禁止访问私有属性');
return Reflect.get(obj, prop, receiver);
},
set(obj, prop, value, receiver) {
if (currentUser.role !== 'admin') throw new Error('无写权限');
return Reflect.set(obj, prop, value, receiver);
}
});
}
节流、防抖本质上也是保护代理——它们控制的是"访问频率"。
四、ES6 Proxy:语言级的代理
Proxy 让代理从"手写同名方法"变成"拦截所有操作",共有 13 个可拦截的操作(trap)。
const target = { name: 'Tom', age: 18 };
const p = new Proxy(target, {
get(obj, prop, receiver) { console.log('read', prop); return Reflect.get(obj, prop, receiver); },
set(obj, prop, value, receiver) { console.log('write', prop); return Reflect.set(obj, prop, value, receiver); },
has(obj, prop) { return Reflect.has(obj, prop); }, // in 操作符
deleteProperty(obj, prop) { return Reflect.deleteProperty(obj, prop); },
ownKeys(obj) { return Reflect.ownKeys(obj); }, // Object.keys / for...in
apply(fn, thisArg, args) { return Reflect.apply(fn, thisArg, args); },
construct(Ctor, args) { return Reflect.construct(Ctor, args); } // new
});
为什么 trap 里要配合 Reflect
Reflect 的方法与 trap 一一对应、参数一致,且返回值语义更合理(Reflect.set 返回布尔而不是抛错)。更关键的是 receiver 参数:它保证 getter/setter 里的 this 指向代理对象而不是原始对象,否则通过原型链继承时会读到错误的值。写 trap 时一律用 Reflect 转发,不要用 obj[prop]。
实战:安全的深层取值
function safeGet(obj) {
return new Proxy(obj, {
get(target, prop) {
const value = Reflect.get(target, prop);
if (value === undefined || value === null) return safeGet({}); // 继续返回可访问的代理
return typeof value === 'object' ? safeGet(value) : value;
}
});
}
// 不会因为中间层缺失而抛错(可选链 ?. 是更简单的方案,这里演示 trap 的用法)
实战:Vue 3 响应式的核心
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key); // 依赖收集
const res = Reflect.get(target, key, receiver);
return typeof res === 'object' && res !== null ? reactive(res) : res; // 惰性递归
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) trigger(target, key); // 派发更新
return result;
},
deleteProperty(target, key) {
const had = Reflect.has(target, key);
const result = Reflect.deleteProperty(target, key);
if (had && result) trigger(target, key);
return result;
}
});
}
Vue 2 的 Object.defineProperty vs Vue 3 的 Proxy:
Object.defineProperty | Proxy | |
|---|---|---|
| 拦截粒度 | 只能拦截已存在的属性,需递归遍历 | 拦截整个对象的操作 |
| 新增/删除属性 | 检测不到,需要 Vue.set / Vue.delete | 原生支持 |
| 数组 | 需要重写 7 个变异方法,且下标赋值无效 | 原生支持 |
| 初始化开销 | 递归遍历所有属性,深对象很慢 | 惰性代理,访问到才递归 |
| 兼容性 | IE9+ | 不支持 IE,且无法 polyfill |
五、代理 vs 装饰器 vs 适配器
三者结构相似(都包一层),区别在意图:
| 模式 | 意图 | 接口是否改变 |
|---|---|---|
| 代理 | 控制访问(延迟、缓存、鉴权、限流) | 不变 |
| 装饰器 | 增强功能(在原功能上叠加新行为) | 不变(可扩展) |
| 适配器 | 转换接口(让不兼容的东西能配合) | 改变 |
一句话区分:代理是"要不要让你调",装饰器是"调完再多做点",适配器是"把插头改成能插的形状"。
下一步 👉 装饰器模式
