Proxy 模式

用一个代理对象拦截对目标对象的访问,在不修改目标对象的前提下控制或扩展其行为。与 Decorator 的区别:Decorator 增强行为,Proxy 通常用于透明转发、访问控制或懒加载。

定义

Proxy 模式用一个代理对象包装真实对象,拦截对其属性和方法的访问。调用方看到的是代理,但实际行为可以在代理层被修改、拦截或转发。

与 Decorator、Adapter 的核心区别

ProxyDecoratorAdapter
意图控制访问(懒加载、访问控制、版本兼容)增强行为(添加功能、扩展能力)接口转换(让不兼容接口协作)
接口与被代理对象完全相同通常扩展或增强原接口目标接口与源接口不同
运行时拦截特定属性/方法,其余透传在调用前后添加逻辑将调用转换成另一种形式
持有引用持有原对象,选择性透传持有内层对象,调用后增强持有被适配对象,转换调用

在 TypeScript 框架里最容易混淆的场景:所有三种模式都是"包装另一个对象",区分点在于为什么包装

vercel/ai 中的两种 Proxy 变体

轻量 Proxy:V3 → V4(as-language-model-v4.ts:L19,25 行)

typescript
return new Proxy(v3Model, {
  get(target, prop) {
    if (prop === 'specificationVersion') return 'v4';  // 唯一修改
    return target[prop];                               // 全部透传
  },
}) as unknown as LanguageModelV4;

只拦截 1 个属性specificationVersion),其余全部透传。零运行时开销,V3 和 V4 数据结构完全兼容,只是类型名不同——纯类型系统问题,Proxy 只是为了让 TypeScript 编译器满意。

重量 Proxy:V2 → V3(as-language-model-v3.ts,103 行)

拦截 3 个属性specificationVersion + doGenerate + doStream

doGenerate 需要做真实的结构转换——V2 和 V3 的 usage 字段结构不同:

typescript
// V2 usage(平铺)
{ inputTokens: number, outputTokens: number }

// V3 usage(嵌套,更细粒度)
{ inputTokens: { total, noCache, cacheRead, cacheWrite }, outputTokens: { total, text, reasoning } }

doStream 有一个 P1 技术债(as-language-model-v3.ts:L72):stream chunk 的 default 分支直接强制转换(chunk as LanguageModelV3StreamPart),不做事件映射。V2 中不存在的 stream 事件类型(如 V3 新增的 reasoning 系列)会被静默传递错误格式数据。

结论:V2→V3 是真正的数据模型演化(usage 结构重构),V3→V4 是接口合同重构(返回类型可寻址化),两次升级解决的是完全不同的问题。

TypeScript Proxy 的类型弱点

as unknown as TargetType 是 JavaScript Proxy 在 TypeScript 中的系统性代价——编译器无法自动推断 Proxy 的返回类型符合目标接口,必须强制转换。这在两个变体中都出现,是 Proxy 模式在 TypeScript 里的固有局限,不是设计失误。