代理模式

为一个对象提供一个替身,以便控制对它的访问。

代理与本体实现相同的接口,因此对使用方完全透明——使用方以为自己在直接操作本体。

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);   // 命中缓存

生产可用的缓存代理需要考虑四件事

  1. key 的生成:JSON.stringify 对参数顺序敏感({a:1,b:2} 与 {b:2,a:1} 不同),且无法处理函数、undefined、循环引用。复杂场景要自定义 resolver。
  2. 缓存淘汰:Map 会无限增长导致内存泄漏,需要 LRU 或 TTL。只以对象为 key 且允许被回收时,用 WeakMap。
  3. 异步与失败:缓存的是 Promise 的话,rejected 的 Promise 必须从缓存中删掉,否则一次网络抖动会被永久缓存。
  4. 失效时机:数据变更后如何清除缓存,这通常比缓存本身更难。

请求去重就是缓存代理的一个变种:

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.definePropertyProxy
拦截粒度只能拦截已存在的属性,需递归遍历拦截整个对象的操作
新增/删除属性检测不到,需要 Vue.set / Vue.delete原生支持
数组需要重写 7 个变异方法,且下标赋值无效原生支持
初始化开销递归遍历所有属性,深对象很慢惰性代理,访问到才递归
兼容性IE9+不支持 IE,且无法 polyfill

五、代理 vs 装饰器 vs 适配器

三者结构相似(都包一层),区别在意图:

模式意图接口是否改变
代理控制访问(延迟、缓存、鉴权、限流)不变
装饰器增强功能(在原功能上叠加新行为)不变(可扩展)
适配器转换接口(让不兼容的东西能配合)改变

一句话区分:代理是"要不要让你调",装饰器是"调完再多做点",适配器是"把插头改成能插的形状"。


下一步 👉 装饰器模式

上次更新:
贡献者: Joe