Proxy 模式
用一个代理对象拦截对目标对象的访问,在不修改目标对象的前提下控制或扩展其行为。与 Decorator 的区别:Decorator 增强行为,Proxy 通常用于透明转发、访问控制或懒加载。
定义
Proxy 模式用一个代理对象包装真实对象,拦截对其属性和方法的访问。调用方看到的是代理,但实际行为可以在代理层被修改、拦截或转发。
与 Decorator、Adapter 的核心区别
| Proxy | Decorator | Adapter | |
|---|---|---|---|
| 意图 | 控制访问(懒加载、访问控制、版本兼容) | 增强行为(添加功能、扩展能力) | 接口转换(让不兼容接口协作) |
| 接口 | 与被代理对象完全相同 | 通常扩展或增强原接口 | 目标接口与源接口不同 |
| 运行时 | 拦截特定属性/方法,其余透传 | 在调用前后添加逻辑 | 将调用转换成另一种形式 |
| 持有引用 | 持有原对象,选择性透传 | 持有内层对象,调用后增强 | 持有被适配对象,转换调用 |
在 TypeScript 框架里最容易混淆的场景:所有三种模式都是"包装另一个对象",区分点在于为什么包装。
vercel/ai 中的两种 Proxy 变体
轻量 Proxy:V3 → V4(as-language-model-v4.ts:L19,25 行)
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 字段结构不同:
// 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 里的固有局限,不是设计失误。