你大概写过这样的代码:页面需要加载一张高清大图,用户一进来就开始下载,哪怕那张图在屏幕下方三屏之外。结果首屏卡了,用户早就划走了。
问题不在图片本身,而在于——你把"是否加载"的决策权交给了错误的人。
这就是 Proxy 模式要解决的核心问题。
一、经典困境:当"立刻创建"变成负担
Refactoring.Guru 的原文用了一个很直白的例子:你有一个消耗大量资源的重量级对象(比如数据库连接),但你只是偶尔需要它。
Proxy 问题示意:重量级对象一创建就占满资源
最直觉的做法是"延迟初始化"——用到才创建。但如果把延迟逻辑散落在所有调用方里,你会发现每个地方都在重复写 if (!instance) { instance = create() }。
更糟的是,如果这个重量级对象来自一个封闭的第三方库,你根本改不了它的源码。
问题从来不是"要不要延迟",而是"这段控制逻辑该放在谁身上"。
二、Proxy 的核心思路
答案是:加一个中间人。
Proxy 模式的做法很简单——创建一个和真实对象接口完全一样的代理类。客户端以为自己在和真实对象打交道,其实它面对的是代理。代理收到请求后,可以做各种"门卫"工作:延迟创建、权限校验、缓存结果、记录日志。完事之后,再把请求转给真实对象。
Proxy 解决方案:代理在客户端和真实对象之间做中介
这里有一个关键点:Proxy 和被代理对象必须实现同一个接口。 这不是巧合,而是设计上的硬约束——只有接口一致,客户端才能在毫不知情的情况下被"换人"。
原文有个绝佳的类比:信用卡就是银行账户的代理。 信用卡和现金实现了相同的"接口"——都能付款。但信用卡在付款之前,悄悄做了安全验证、额度检查、交易记录。消费者不需要知道背后的复杂流程。
信用卡是银行账户的代理,实现相同的"支付"接口
三、结构拆解
Proxy 模式结构图
| 角色 | 职责 |
|---|---|
| Service Interface | 声明统一接口,代理和真实对象都要遵守 |
| Service | 真正干活的重量级对象 |
| Proxy | 持有 Service 引用,在请求前后插入控制逻辑 |
| Client | 只认接口,不关心拿到的是代理还是真实对象 |
代理的权力边界很明确:它控制的是"入口",不是"行为"。
四、前端的四个典型代理场景
1. 图片懒加载代理
这是前端最常见的 Proxy 场景。图片组件的"真实对象"是 <img src="...">,但你不想让它一挂载就开始下载。
// ✅ 图片懒加载代理
class LazyImage {
private realSrc: string
private img: HTMLImageElement
constructor(src: string) {
this.realSrc = src
this.img = document.createElement('img')
this.img.src = 'placeholder.svg' // 先放占位图
}
// 只有进入视口才触发真实加载
load() {
this.img.src = this.realSrc
}
}
// 配合 IntersectionObserver,进入视口再调 load()
代理在这里做的事情是:控制"何时触达"真实资源。 客户端(页面布局)只管放图片组件,什么时候真正下载,代理说了算。
用户看到的是同一个图片组件,但加载时机已经不由它自己决定了。
2. API 缓存代理
重复请求同一个接口?直接加个缓存层。
// ✅ API 缓存代理
class CachedApi {
private cache = new Map<string, { data: any; expiry: number }>()
private ttl = 60_000 // 缓存 1 分钟
async get(url: string) {
const cached = this.cache.get(url)
if (cached && Date.now() < cached.expiry) {
return cached.data // 命中缓存,不走网络
}
const data = await fetch(url).then(r => r.json())
this.cache.set(url, { data, expiry: Date.now() + this.ttl })
return data
}
}
这个模式和原文伪代码中的 CachedYouTubeClass 完全对应。真实的 fetch 是重量级操作(网络 I/O),代理在前面拦一层——有缓存就不走网络,没缓存才放行。
代理不改变请求的结果,它只决定结果从哪儿来。
3. 权限控制代理
后台管理系统里,不同角色能看到的操作按钮不一样。与其在每个组件里散落 if (role === 'admin') 的判断,不如把权限校验集中到代理层。
// ✅ 权限代理
class PermissionProxy<T extends object> {
constructor(
private target: T,
private role: string,
private whitelist: Record<string, string[]>
) {}
call<K extends keyof T>(method: K, ...args: any[]) {
const allowed = this.whitelist[method as string] || []
if (!allowed.includes(this.role)) {
throw new Error(`无权执行 ${String(method)}`)
}
return (this.target[method] as Function)(...args)
}
}
代理在这里充当的是保护代理:接口一样,但不是谁都能通过。
4. 第三方 SDK 访问代理
接入一个第三方埋点 SDK,你不希望它在测试环境真的发请求,也不希望业务代码直接依赖它的全局变量。
// ✅ 埋点 SDK 代理
class AnalyticsProxy {
private sdk: RealAnalyticsSDK | null = null
private ensureSDK() {
if (!this.sdk && isProduction()) {
this.sdk = new RealAnalyticsSDK(config)
}
}
track(event: string, data: Record<string, any>) {
this.ensureSDK()
if (this.sdk) {
this.sdk.track(event, data)
} else {
console.log('[Analytics Mock]', event, data)
}
}
}
这同时融合了虚拟代理(延迟初始化)和保护代理(环境判断)。业务代码只管调 analytics.track(),至于背后是真发还是假发,代理说了算。
五、Proxy vs Decorator vs Adapter:到底怎么分?
这三个模式长得很像,面试也常考。区别其实可以用一张表讲清楚:
| 维度 | Proxy | Decorator | Adapter |
|---|---|---|---|
| 接口 | 和目标完全相同 | 和目标完全相同 | 和目标不同 |
| 核心意图 | 控制访问 | 增强行为 | 转换接口 |
| 谁决定组合 | 代理自己管理目标的生命周期 | 客户端控制装饰链 | 客户端传入被适配对象 |
| 隐喻 | 门卫 | 套娃 | 转接头 |
一句话区分:Adapter 换了张脸,Decorator 加了件衣服,Proxy 守了一道门。
六、深层价值:关于"控制入口"的系统思维
从更高的视角看,Proxy 模式的本质和建筑学中的玄关(foyer) 设计异曲同工。你家的门不是直接通向客厅,中间有一个过渡区域——玄关。在这个过渡区里,你可以换鞋、放包、拦住推销员。客厅(真实对象)保持干净,因为脏活被挡在了入口。
在经济学里,这叫交易成本的内化。Proxy 把原本散布在每个调用方的"控制成本"集中到了一个明确的中间层,降低了系统的总协调成本。
好的系统设计从来不是减少复杂度,而是把复杂度放对地方。
七、什么时候不该用 Proxy
Proxy 不是万能的。你需要警惕:
• 过度代理:简单的直接调用非要包一层代理,徒增复杂度和调用栈深度
• 代理泄漏:代理开始"偷偷"修改请求参数或返回结果,变成了事实上的 Decorator/Adapter,角色越界
• 调试黑洞:代理链太长时,出了 bug 你很难定位是代理层的问题还是真实对象的问题
判断清单:
✅ 需要控制访问时机(延迟、缓存、节流)→ 用 Proxy
✅ 需要控制访问权限(角色、环境、频率)→ 用 Proxy
❌ 需要改变接口形状 → 那是 Adapter 的活
❌ 需要叠加额外行为(日志 + 监控 + 重试层层套) → 更像 Decorator
八、一句话带走
如果你只想记一个核心观点:
Proxy 不改变你做什么,它决定你能不能做、什么时候做、从哪儿拿结果。把"控制入口"的职责集中到一个明确的中间层,业务代码才能保持干净。
它和上周聊的 Adapter 最大的区别:Adapter 解决的是"翻译",两边语言不通;Proxy 解决的是"门禁",语言相同但不是谁都能进。
一个好的代理层不会让调用方多想一秒,但它在暗中替你挡掉了所有不该到达真实对象的请求。
参考原文:
• Refactoring.Guru — Proxy