讲解

明星不直接接电话,经纪人先接:筛掉骚扰(保护)、安排档期(延迟真实动作)、记录来电(附加职责)。经纪人对外就是「明星的联系方式」——调用方觉得在打给明星,实际先过了经纪人。代理模式就是这样:给某个对象提供一个替身,替身和本尊实现同一接口,客户端无感,替身在转发调用的时机里做控制。

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 是语言级拦截,思想相通但层次不同。结构型七种至此完结,下一章进入行为型:责任链模式。