Proxy:谁在替你拦第一道门?

0 阅读6分钟

你大概写过这样的代码:页面需要加载一张高清大图,用户一进来就开始下载,哪怕那张图在屏幕下方三屏之外。结果首屏卡了,用户早就划走了。

问题不在图片本身,而在于——你把"是否加载"的决策权交给了错误的人。

这就是 Proxy 模式要解决的核心问题。

一、经典困境:当"立刻创建"变成负担

Refactoring.Guru 的原文用了一个很直白的例子:你有一个消耗大量资源的重量级对象(比如数据库连接),但你只是偶尔需要它。

Proxy 问题示意:重量级对象一创建就占满资源

Proxy 问题示意:重量级对象一创建就占满资源

最直觉的做法是"延迟初始化"——用到才创建。但如果把延迟逻辑散落在所有调用方里,你会发现每个地方都在重复写 if (!instance) { instance = create() }

更糟的是,如果这个重量级对象来自一个封闭的第三方库,你根本改不了它的源码。

问题从来不是"要不要延迟",而是"这段控制逻辑该放在谁身上"。

二、Proxy 的核心思路

答案是:加一个中间人

Proxy 模式的做法很简单——创建一个和真实对象接口完全一样的代理类。客户端以为自己在和真实对象打交道,其实它面对的是代理。代理收到请求后,可以做各种"门卫"工作:延迟创建、权限校验、缓存结果、记录日志。完事之后,再把请求转给真实对象。

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:到底怎么分?

这三个模式长得很像,面试也常考。区别其实可以用一张表讲清楚:

维度ProxyDecoratorAdapter
接口和目标完全相同和目标完全相同和目标不同
核心意图控制访问增强行为转换接口
谁决定组合代理自己管理目标的生命周期客户端控制装饰链客户端传入被适配对象
隐喻门卫套娃转接头

一句话区分:Adapter 换了张脸,Decorator 加了件衣服,Proxy 守了一道门。

六、深层价值:关于"控制入口"的系统思维

从更高的视角看,Proxy 模式的本质和建筑学中的玄关(foyer)  设计异曲同工。你家的门不是直接通向客厅,中间有一个过渡区域——玄关。在这个过渡区里,你可以换鞋、放包、拦住推销员。客厅(真实对象)保持干净,因为脏活被挡在了入口。

在经济学里,这叫交易成本的内化。Proxy 把原本散布在每个调用方的"控制成本"集中到了一个明确的中间层,降低了系统的总协调成本。

好的系统设计从来不是减少复杂度,而是把复杂度放对地方。

七、什么时候不该用 Proxy

Proxy 不是万能的。你需要警惕:

• 过度代理:简单的直接调用非要包一层代理,徒增复杂度和调用栈深度

• 代理泄漏:代理开始"偷偷"修改请求参数或返回结果,变成了事实上的 Decorator/Adapter,角色越界

• 调试黑洞:代理链太长时,出了 bug 你很难定位是代理层的问题还是真实对象的问题

判断清单:

✅ 需要控制访问时机(延迟、缓存、节流)→ 用 Proxy

✅ 需要控制访问权限(角色、环境、频率)→ 用 Proxy

❌ 需要改变接口形状 → 那是 Adapter 的活

❌ 需要叠加额外行为(日志 + 监控 + 重试层层套) → 更像 Decorator

八、一句话带走

如果你只想记一个核心观点:

Proxy 不改变你做什么,它决定你能不能做、什么时候做、从哪儿拿结果。把"控制入口"的职责集中到一个明确的中间层,业务代码才能保持干净。

它和上周聊的 Adapter 最大的区别:Adapter 解决的是"翻译",两边语言不通;Proxy 解决的是"门禁",语言相同但不是谁都能进。

一个好的代理层不会让调用方多想一秒,但它在暗中替你挡掉了所有不该到达真实对象的请求。

参考原文:

• Refactoring.Guru — Proxy

qrcode_for_gh_6a9e7f3719d6_344.jpg