讲解
明星不直接接电话,经纪人先接:筛掉骚扰(保护)、安排档期(延迟真实动作)、记录来电(附加职责)。经纪人对外就是「明星的联系方式」——调用方觉得在打给明星,实际先过了经纪人。代理模式就是这样:给某个对象提供一个替身,替身和本尊实现同一接口,客户端无感,替身在转发调用的时机里做控制。
GoF 把代理按用途分成几类,实战中最常见三种。虚拟代理(virtual proxy):延迟昂贵对象的创建,比如图片列表只在真正滚动到可视区时才加载原图,先用占位代理顶着。保护代理(protection proxy):在调用前做权限检查,无权限直接拒绝。缓存代理:同样参数的请求直接返回缓存结果(装饰器章节的缓存例子其实就是代理的近亲)。此外还有远程代理(RPC stub,本地对象冒充远程服务)、日志代理等。
代理和装饰器结构几乎一样(都是同接口包装 + 转发),区分的仍是意图:装饰器给对象「加职责」(叠加新能力),代理给对象「做管控」(控制访问的时机、权限、成本)。同一个包装类常常身兼两职,不必教条;但命名和文档里说明意图,读代码的人会感谢你的。
JavaScript/TypeScript 还有一个语言级的 Proxy 对象(ES2015 内建),能拦截属性读写、方法调用等几乎所有操作——Vue 3 的响应式系统就靠它。它和 GoF 代理模式不是一回事(一个拦语言操作,一个是对象结构),但思想相通:在访问的必经之路上插一道关卡。示例里两种都演示。
示例
虚拟代理延迟加载图片 + 语言内建 Proxy 做属性读取日志,各演示一遍:
import assert from 'node:assert/strict';
interface Image {
render(): string;
}
// 真实对象:构造很贵(用静态计数模拟「从磁盘加载」)
class RealImage implements Image {
static loads = 0;
constructor(private readonly filename: string) {
RealImage.loads++;
}
render(): string {
return '显示图片 ' + this.filename;
}
}
// 虚拟代理:同接口,第一次 render 时才真正创建本尊
class LazyImageProxy implements Image {
private real: RealImage | null = null;
constructor(private readonly filename: string) {}
render(): string {
if (this.real === null) {
this.real = new RealImage(this.filename);
}
return this.real.render();
}
}
const img = new LazyImageProxy('cat.png');
assert.equal(RealImage.loads, 0); // 代理在手,本尊还没加载
assert.equal(img.render(), '显示图片 cat.png');
assert.equal(img.render(), '显示图片 cat.png');
assert.equal(RealImage.loads, 1); // 多次 render,只加载一次
// 语言级 Proxy:拦截属性读取,自动记日志
const logs: string[] = [];
const account = { balance: 100 };
const observed = new Proxy(account, {
get(obj, prop: string | symbol): unknown {
logs.push('读取 ' + String(prop));
return Reflect.get(obj, prop) as unknown;
},
});
const value = (observed as { balance: number }).balance;
assert.equal(value, 100);
assert.deepEqual(logs, ['读取 balance']);
console.log('虚拟代理:render 两次,真实加载 ' + RealImage.loads + ' 次');
console.log('内建 Proxy 拦截日志:' + logs.join(', '));
前四个断言把虚拟代理的语义钉死了:拿到代理不触发加载,第一次使用时才加载,之后复用。后半段的内建 Proxy 展示了另一种「关卡」:任何对 observed.balance 的读取都先经过 get 拦截器。响应式框架正是把所有属性读写都这样拦截下来做依赖收集的。
常见坑
- 代理里缓存了本尊的可变快照:本尊状态变了,代理还返回旧快照;缓存代理必须有失效策略(TTL、主动失效)。
- 代理层泄漏具体类型:调用方判断 instanceof RealImage 来做分支,代理就白架了;客户端代码应只面向接口。
- 层层代理无人知道:代理叠代理再叠装饰器,调用链七八层,排查问题像拆俄罗斯套娃;装配处要有一张「包装顺序图」。
- 内建 Proxy 滥用:拦截器里写复杂逻辑会让每次属性访问都付出成本且难以调试;用它做框架级基础设施,别拿它写业务。
- 保护代理返回含糊错误:权限不足时应抛出带上下文的具体异常(谁、想干什么、缺什么权限),而不是静默返回空数据。
小结
代理 = 同接口替身 + 访问管控,常见形态有虚拟(延迟)、保护(权限)、缓存代理;意图上与装饰器(叠加职责)相区分。JS 内建 Proxy 是语言级拦截,思想相通但层次不同。结构型七种至此完结,下一章进入行为型:责任链模式。