Babel 中 TC39 标准轨道的装饰器
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
Babel 7.1.0 终于支持了新的装饰器提案:您可以通过 @babel/plugin-proposal-decorators 插件进行体验 🎉。
历史回顾
装饰器由 Yehuda Katz 在三年前首次提出。TypeScript 在 1.5 版本(2015年)随众多 ES6 特性一同发布了装饰器支持。 Angular、MobX 等主流框架开始使用它们来提升开发者体验:这使得装饰器广受欢迎,同时也让社区产生了稳定性的错觉。
Babel 最初在 5.0 版本实现了装饰器,但在 Babel 6 中移除了该功能,因为提案当时仍处于变动状态。Logan Smyth 创建了非官方插件 (babel-plugin-transform-decorators-legacy) 来复现 Babel 5 的行为;该插件在 Babel 7 的首个 alpha 版本期间被移至官方仓库。此插件仍采用旧版装饰器语义,因为当时新提案的具体形式尚未明确。
此后,Daniel Ehrenberg 和 Brian Terlson 与 Yehuda Katz 共同成为提案作者,提案几乎被完全重写。截至目前,并非所有细节都已确定,也没有完全合规的实现。
Babel 7.0.0 为 @babel/plugin-proposal-decorators 插件引入了新标志:legacy 选项,当时其唯一有效值为 true。此项破坏性变更是为了提供从 Stage 1 提案版本到当前版本的平滑迁移路径。
在 Babel 7.1.0 中,我们正式支持这项新提案,且在使用 @babel/plugin-proposal-decorators 插件时默认启用。若未在 Babel 7.0.0 引入 legacy: true 选项,我们将无法默认使用正确的语义(即等效于 legacy: false)。
新提案还支持在私有字段和方法上使用装饰器。Babel 尚未实现此功能(每个类中装饰器与私有元素不可同时使用),但即将推出。
新提案有哪些变化?
尽管新提案与旧版极为相似,但存在几项关键差异导致二者互不兼容。
语法
旧提案允许任何有效的左侧表达式(字面量、函数与类表达式、new 表达式和函数调用、简单属性访问和计算属性访问)作为装饰器主体。例如以下代码是合法的:
class MyClass {
@getDecorators().methods[name]
foo() {}
@decorator
[bar]() {}
}
该语法存在一个问题:[...] 符号在装饰器体内既用于属性访问,又用于定义计算属性名。为避免歧义,新提案仅允许点属性访问(foo.bar),可选择在末尾添加参数(foo.bar())。如需使用更复杂的表达式,可用括号包裹:
class MyClass {
@decorator
@dec(arg1, arg2)
@namespace.decorator
@(complex ? dec1 : dec2)
method() {}
}
对象装饰器
旧版提案除了支持类和类元素装饰器外,还允许对象成员装饰器:
const myObj = {
@dec1 foo: 3,
@dec2 bar() {},
};
由于与当前对象字面量语义存在兼容性问题,该特性已从提案中移除。若您的代码正在使用此特性,请保持关注,后续提案可能会重新引入(详见 tc39/proposal-decorators#119)。
装饰器函数参数
新提案的第三项重要变更是装饰器函数的参数传递机制。
在初始提案中,类元素装饰器接收三个参数:目标类(或对象)、属性键和属性描述符(形似 Object.defineProperty 的参数)。类装饰器则仅接收目标构造函数作为参数。
新版装饰器提案功能更为强大:元素装饰器接收的对象不仅可修改属性描述符,还能调整属性键、放置位置(static/prototype/own)和元素类型(field或method)。它们还能创建额外属性,并定义在装饰类时执行的函数(称为 finisher)。
类装饰器则接收包含所有类元素描述符的对象,支持在类创建前进行整体修改。
升级指南
由于这些不兼容性,现有装饰器无法直接用于新提案:这将导致迁移过程极为缓慢,因为现有库(MobX、Angular 等)必须引入破坏性变更才能升级。 为解决此问题,我们发布了可包装现有装饰器的工具包。运行后即可安全切换 Babel 配置使用新提案 🎉。
您可通过单行命令升级文件:
npx wrap-legacy-decorators src/file-with-decorators.js --decorators-before-export --write
若代码仅在 Node 环境运行,或使用 Webpack/Rollup 打包,可通过外部依赖避免在每个文件中注入包装函数:
npm install --save decorators-compat
npx wrap-legacy-decorators src/file-with-decorators.js --decorators-before-export --external-helpers --write
更多详情请参阅工具包文档。
未决问题
装饰器作为复杂特性,其最佳实现方案仍存争议,部分设计尚未最终确定。
导出类的装饰器位置
提案对此问题反复讨论:装饰器应置于 export 关键字之前还是之后?
export @decorator class MyClass {}
// or
@decorator
export class MyClass {}
核心争议在于 export 是否属于类声明的一部分:若视为声明组成部分,装饰器应居前(因装饰器位于声明起始处);若视为"包装器",则应居后(因装饰器属于类声明内容)。
装饰器与私有元素的安全交互
装饰器引发重要安全隐患:若允许装饰私有元素,则私有名称(可视作私有元素的"密钥")可能泄露。需考量不同安全层级:
-
装饰器不应意外泄露私有名称。恶意代码绝不能以任何方式窃取其他装饰器的私有名称。
-
仅直接应用于私有元素的装饰器可视为可信:类装饰器是否应被禁止读写私有元素?
-
强隐私性(类字段提案的目标之一)意味着私有元素只能在类内部访问:装饰器是否应该能访问私有名称?是否只能装饰公共元素?
这些问题在解决前需要进一步讨论,而这正是 Babel 发挥作用的地方。
Babel 的角色
延续管道提案(|>)进展如何?一文的趋势,随着 Babel 7 的发布,我们正利用在 JS 生态中的位置,通过让开发者测试提案的不同变体并提供反馈,从而更深入地帮助提案作者。
因此,在更新 @babel/plugin-proposal-decorators 的同时,我们引入了新选项:decoratorsBeforeExport,该选项允许用户同时尝试 export @decorator class C {} 和 @decorator export default class 两种写法。
我们还将引入选项来自定义被装饰私有元素的隐私约束。在 TC39 团队做出决定前,这些选项将是必需的,这样我们就能让默认行为与最终提案保持一致。
如果直接使用我们的解析器 (@babel/parser,原名为 babylon),你已在 7.0.0 版本中可以使用 decoratorsBeforeExport 选项:
const ast = babylon.parse(code, {
plugins: [
["decorators", { decoratorsBeforeExport: true }]
]
})
使用方式
在 Babel 中的使用方式:
- npm
- Yarn
- pnpm
- Bun
npm install @babel/plugin-proposal-decorators --save-dev
yarn add @babel/plugin-proposal-decorators --dev
pnpm add @babel/plugin-proposal-decorators --save-dev
bun add @babel/plugin-proposal-decorators --dev
{
"plugins": ["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }]
}
查看 @babel/plugin-proposal-decorators 文档获取更多选项。
你的角色
作为 JavaScript 开发者,你可以帮助塑造这门语言的未来。通过测试装饰器正在考虑的各种语义,并向提案作者反馈实际使用经验。我们需要了解你在真实项目中的使用场景!你也可以通过阅读提案仓库中的 issue 讨论和会议记录,了解设计决策背后的原因。
若想立即尝试装饰器,可以在我们的 repl 中体验不同预设选项!