从“实验特性”到“语言标准”
很多 JavaScript 开发者第一次接触“装饰器”这个词,多半不是在 TC39 提案文档里,而是在 TypeScript 的 experimentalDecorators 开关、Angular 的 @Component、NestJS 的 @Controller,或者 MobX 的 @observable 中。装饰器这种写法看似简单,却一直处于一种“广被使用,却始终游离在标准之外”的状态。直到 TC39 将装饰器提案推进到 Stage 3,这种状态才真正开始松动。

Stage 3 的意义,不在于又多了一个花哨语法,而在于 JavaScript 类的能力,将在语言层面获得一次系统性的元编程升级。并且,这次升级经历了远比大多数人想象的更漫长的讨论和反复。
装饰器到底解决了什么问题
装饰器解决的核心问题,是把“横向关注点”从类定义中抽离出来。比如日志、权限、依赖注入、时间测量、绑定 this,这些逻辑如果散落在每个方法里,会非常冗余。装饰器允许你在类定义时,通过一段声明式代码修改类的行为。
这不是简单的语法糖。如果只是语法糖,高阶函数也能实现类似效果。但装饰器是类定义阶段的一种钩子,它在类被创建时运行,可以替换方法、初始化字段、改变原型,甚至可以影响私有元素的语义。它服务的对象是类,而不是普通函数,这一点是理解整个提案的关键。
从旧装饰器到新提案:一个并不顺利的过程
TypeScript 早在 1.5 就支持了 experimentalDecorators,但那些年装饰器一直作为实验特性存在。早期提案也沿用了 TS 的语义,允许对函数和类表达式使用装饰器,上下文对象结构也比较简单。但 TC39 在讨论中发现,这种设计对静态分析、引擎优化和兼容性都有隐患,尤其是字段装饰器在定义时修改字段值的方式,很容易引发性能陷阱。
最终进入 Stage 3 的提案,采用了更内敛的设计:装饰器只能用于类和类成员,不能用于普通函数;字段装饰器只能做初始化钩子,不能直接修改字段初始值;新增 addInitializer 来补充初始化逻辑。这些变化让装饰器更可预测,但也意味着从 TS 旧装饰器迁移并不是无缝的。
一个简单的标准装饰器长什么样
基于当前 Stage 3 草案,装饰器是一个接收 value 和 context 两个参数的函数。下面用一个日志装饰器来感受一下:
function logged(value, context) {
if (context.kind === 'method') {
return function (...args) {
console.log('calling ' + context.name + ' with', args);
const result = value.apply(this, args);
console.log('finished', result);
return result;
};
}
}
class UserService {
@logged
getUser(id) {
return { id, name: 'Alice' };
}
}
在这个例子里,logged 接收两个参数:value 是原始方法,context 描述了这个成员的信息,比如它是方法还是 getter,叫什么名字,是不是静态成员。装饰器在类定义时被调用,返回的新函数会覆盖原方法。这种模式看起来和包装函数很像,但关键在于它发生在类定义阶段,并且由语言提供统一机制。
需要强调,这种写法与 TypeScript 旧装饰器的语义有很大差别。旧装饰器面对的是属性描述符,新装饰器面对的是一个明确的上下文对象;旧装饰器支持参数装饰器,新提案则没有。如果你在网上搜到一段“装饰器代码”,一定要先分辨它是 TS 旧风格还是标准风格。
标准装饰器、旧装饰器与高阶函数:三张不同的脸
为了更清楚地看出差异,我把标准装饰器、TypeScript 旧装饰器和高阶函数放在一起对比:
| 维度 | 标准装饰器(Stage 3) | TypeScript 旧装饰器 | 高阶函数 |
|---|---|---|---|
| 作用对象 | 类及类成员 | 类、类成员、参数 | 任何函数 |
| 是否标准化 | TC39 Stage 3 | TypeScript 实验特性 | ECMAScript 标准 |
| 字段装饰器行为 | 通过初始化器钩子间接修改 | 可直接操作描述符或初始值 | 不适用 |
| 元数据能力 | 通过 context.metadata 读取和写入 | 依赖 emitDecoratorMetadata | 无 |
| 迁移兼容性 | 未来标准,生态逐步跟进 | 广泛存在,但与标准不兼容 | 无兼容风险 |
这里有一个很现实的问题:旧装饰器虽然灵活,但灵活性也意味着不确定性。标准装饰器刻意收窄了“可以做的手脚”,换来了引擎优化空间和语义稳定性。对于框架作者来说,元数据能力是依赖注入的基础;对于业务开发者,可能几年都用不到一次。
几个常见误区
- 误区一:装饰器可以装饰任意函数。标准装饰器只针对类和类成员,普通函数声明不能被装饰。这个限制不是偷懒,而是只有类提供了足够的定义时性上下文,装饰器才能安全地介入。
- 误区二:装饰器就是高阶函数的语法糖。高阶函数只能在调用时包装行为,装饰器则能参与类构造过程,比如注册元数据、影响原型。很多框架的依赖注入,依赖的是装饰器附加的元数据,而不只是返回新函数。
- 误区三:TypeScript 旧装饰器代码可以无缝升级。实际迁移需要改不少东西,比如参数装饰器的处理方式、字段装饰器语义变化、以及 emitDecoratorMetadata 依赖的反射设计。
这些误区在团队协作中很常见。有人觉得“先上装饰器再说”,结果发现标准一更新,旧代码变成历史包袱;也有人把装饰器用在普通函数里,编译不过才发现走错了方向。
团队应该怎么面对装饰器
先看一个现实场景。如果你在维护 NestJS 这类重度装饰器框架,你会发现框架内部大量使用 @Controller、@Injectable 来声明依赖关系。这些装饰器在编译阶段收集元数据,本质上是在类定义时建立一张隐式的注册表。TC39 标准化的推进,对这类框架是重要信号:至少语言层面的预期稳定了。但迁移到标准装饰器不是零成本,框架作者需要兼容旧的编译路径和用户代码。
另一个场景来自前端状态管理。不少团队在 MobX 早期版本里用过 @observable 装饰器,后来迁移到 Hooks 后就不再接触装饰器。这类团队重新看到装饰器时,容易把它理解成“给类方法包一层”,忽略它真正的元编程能力。而元编程能力,恰恰是框架作者最依赖的东西。
落到具体行动上,我给下面几条建议:
- 如果你的业务代码重度依赖 NestJS、Angular 等框架,继续跟随框架的升级节奏,暂时不要自己编写基于旧装饰器的抽象层。
- 如果你在维护开源库,可以开始研究标准装饰器的语义,设计兼容迁移的中间层,但不必急着完全切换。
- 如果是新项目,并且使用 TypeScript 5.0 以上,可以尝试关闭 experimentalDecorators,用标准装饰器写新代码。前提是你依赖的框架支持这种模式。
- 无论哪种情况,都不要假设装饰器可以复制粘贴。同一个 @符号,在不同编译配置下可能是两种完全不同的语义。
另外要强调,目前主流浏览器还没有原生支持装饰器,生产环境仍需要通过 TypeScript 或 Babel 转译。所以“标准定稿”不等于“马上可用”,而是一个生态同步的起点。
为什么说这是一个里程碑
装饰器提案从早期讨论到进入 Stage 3,跨度接近十年。它之所以这么慢,是因为它直接挑战了 JavaScript 类模型的设计边界:如何在静态可分析性和动态灵活性之间取得平衡。Stage 3 意味着规范文本基本冻结,后续主要是收集实现反馈和修复边缘 case。对于语言特性来说,这是一个“只减不增结构性变化”的阶段。
从这个角度看,装饰器提案进入 Stage 3,是 JavaScript 元编程能力逐渐成熟的重要信号。它让类不再只是语法糖,而成为可以承载框架约束的基础设施。Java 有注解,Python 有装饰器,TypeScript 早就有实验版,现在 JavaScript 正以一种更克制的方式补上这一课。
当然,距离浏览器原生支持、生态全面迁移还有很长的路。但方向已经明确。对于普通开发者,你不需要立刻把代码改成装饰器,但值得理解它解决什么问题、边界在哪里。元编程能力是一个成熟语言的必然方向,而 JavaScript 正在认真补上这一课。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/554/