跳至主内容

Babel 中 TC39 标准轨道的装饰器

· 1 分钟阅读
非官方测试版翻译

本页面由 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 EhrenbergBrian TerlsonYehuda 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 表达式和函数调用、简单属性访问和计算属性访问)作为装饰器主体。例如以下代码是合法的:

JavaScript
class MyClass {
@getDecorators().methods[name]
foo() {}

@decorator
[bar]() {}
}

该语法存在一个问题:[...] 符号在装饰器体内既用于属性访问,又用于定义计算属性名。为避免歧义,新提案仅允许点属性访问(foo.bar),可选择在末尾添加参数(foo.bar())。如需使用更复杂的表达式,可用括号包裹:

JavaScript
class MyClass {
@decorator
@dec(arg1, arg2)
@namespace.decorator
@(complex ? dec1 : dec2)
method() {}
}

对象装饰器

旧版提案除了支持类和类元素装饰器外,还允许对象成员装饰器:

JavaScript
const myObj = {
@dec1 foo: 3,
@dec2 bar() {},
};

由于与当前对象字面量语义存在兼容性问题,该特性已从提案中移除。若您的代码正在使用此特性,请保持关注,后续提案可能会重新引入(详见 tc39/proposal-decorators#119)。

装饰器函数参数

新提案的第三项重要变更是装饰器函数的参数传递机制。

在初始提案中,类元素装饰器接收三个参数:目标类(或对象)、属性键和属性描述符(形似 Object.defineProperty 的参数)。类装饰器则仅接收目标构造函数作为参数。

新版装饰器提案功能更为强大:元素装饰器接收的对象不仅可修改属性描述符,还能调整属性键、放置位置(static/prototype/own)和元素类型(fieldmethod)。它们还能创建额外属性,并定义在装饰类时执行的函数(称为 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

更多详情请参阅工具包文档

未决问题

装饰器作为复杂特性,其最佳实现方案仍存争议,部分设计尚未最终确定。

导出类的装饰器位置

tc39/proposal-decorators#69

提案对此问题反复讨论:装饰器应置于 export 关键字之前还是之后?

JavaScript
export @decorator class MyClass {}

// or

@decorator
export class MyClass {}

核心争议在于 export 是否属于类声明的一部分:若视为声明组成部分,装饰器应居前(因装饰器位于声明起始处);若视为"包装器",则应居后(因装饰器属于类声明内容)。

装饰器与私有元素的安全交互

tc39/proposal-decorators#129, tc39/proposal-decorators#133

装饰器引发重要安全隐患:若允许装饰私有元素,则私有名称(可视作私有元素的"密钥")可能泄露。需考量不同安全层级:

  1. 装饰器不应意外泄露私有名称。恶意代码绝不能以任何方式窃取其他装饰器的私有名称。

  2. 仅直接应用于私有元素的装饰器可视为可信:类装饰器是否应被禁止读写私有元素?

  3. 强隐私性(类字段提案的目标之一)意味着私有元素只能在类内部访问:装饰器是否应该能访问私有名称?是否只能装饰公共元素?

这些问题在解决前需要进一步讨论,而这正是 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 选项:

JavaScript
const ast = babylon.parse(code, {
plugins: [
["decorators", { decoratorsBeforeExport: true }]
]
})

使用方式

在 Babel 中的使用方式:

npm install @babel/plugin-proposal-decorators --save-dev
babel.config.json
{
"plugins": ["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }]
}

查看 @babel/plugin-proposal-decorators 文档获取更多选项。

你的角色

作为 JavaScript 开发者,你可以帮助塑造这门语言的未来。通过测试装饰器正在考虑的各种语义,并向提案作者反馈实际使用经验。我们需要了解你在真实项目中的使用场景!你也可以通过阅读提案仓库中的 issue 讨论和会议记录,了解设计决策背后的原因。

若想立即尝试装饰器,可以在我们的 repl 中体验不同预设选项!