[en] From TS compiler option useDefineForClassFields
to ES proposal class-fields
[zh] 从 TS 的 useDefineForClassFields
选项到 ES class-fields
提案
#377
Labels
[zh]
[[Set]]
vs[[Define]]
语义useDefineForClassFields
是 TypeScript 3.7.0 中新增的一个编译选项(详见 PR),启用后的作用是将class
声明中的字段语义从[[Set]]
变更到[[Define]]
。我们考虑如下代码:
这是长期以来很常见的一种 TS 字段声明方式,默认情况下它的编译结果如下:
当启用了
useDefineForClassFields
编译选项后它的编译结果如下:可以看到变化主要由如下两点:
=
赋值的方式变更成了Object.defineProperty
默认
=
赋值的方式就是所谓的[[Set]]
语义,因为this.foo = 100
这个操作会隐式地调用上下文中foo
的setter
。相应地Object.defineProperty
的方式即所谓的[[Define]]
语义。在没有
setter
相关的class
中两种语义使用上基本没有区别,但一旦和setter
或继承混合使用时不同的语义就会产生截然不同的效果。考虑如下代码:
class-fields
提案的选择对于字段声明默认赋值为
undefined
相对能获得认可,毕竟是显式地声明了一个字段并且未赋值,类似于不同层级的代码块中声明let value: number
,内层的value
会默认重新创建一个值为undefined
的标识符,因此 TS 中也提供了declare field
的新语法来支持声明字段但不产生实际代码的用法。但初次接触到新的
[[Define]]
语义可能会觉得不可理喻,社区内也有很大的分歧,但实际上 TC39 最终选择了[[Define]]
语义自然有他们的考虑。在上面的例子中,如果是
[[Set]]
语义,data
的setter
被正确触发,但Derived
的实例上并不会拥有一个值为10
的data
属性,即derived.hasOwnProperty('data') === false
且derived.data === undefined
,这『可能』也是不符合预期的。正如 TC39 总结道:
作为替代,TC39 决定在仍处于 stage 2 阶段且『命途多舛』的 decorators 提案中提供一个显式使用
[[Set]]
语义的装饰器。这在我个人看来无疑是可笑的:
[[Set]]
语义,虽然[[Define]]
语义有它实际的价值,但显然从当前的迁移成本来看保留[[Set]]
作为默认语义更合理[[Define]]
语义的实际作用是总是创建类的属性,如果依赖装饰器提案,默认[[Set]]
显式添加类似@define
装饰器来使用[[Define]]
语义影响面更小TC39 的结论可能见仁见智,无法让所有人满意,但 Chrome 已经在版本 72 中发布了基于
[[Define]]
语义的实现,而这个决定几乎不可能被重新考虑了。TS 加速进程
在
class-fields
提案未正式落地之前,TS 仍为用户提供了useDefineForClassFields
编译选项帮助用户之后可以平滑升级,但在 4.0 版本中的一个 bugfix 加速了这个进程。首先回顾一下这个 bug:
如果使用
[[Set]]
语义,Child
实例化的过程中会调用this.foo = 10
,而在基类Base
中foo
只有getter
没有setter
,因此在运行时会抛出异常Cannot set property foo of #<Base> which has only a getter
。TS 4.0 中对这个 bug 修复的方式是『在覆盖属性访问器时一直报错』,不区分是否存在
setter
,简单粗暴,这让一些仍寄希望于useDefineForClassFields
苟延残喘的 TS 用户不得不提前开始一些针对[[Define]]
语义的迁移工作,因为在之前的对比分析中,让[[Set]]
语义支持者不满的地方就是设置子类字段将无法再触发父类的setter
,而 4.0 的这个特性直接禁止了 TS 中的这种写法,当我们把这种模式的代码全部修复后迁移到[[Define]]
语义的成本和风险都将大大降低。这对现有的 TS 项目升级无疑是一个巨大的障碍,但完成迁移后也将推动后续迁移
useDefineForClassFields
默认值,果然优秀的人一直在第五层。Angular 项目中的几种常见迁移方式
直接复用组件
Input
扩展父组件为
getter/setter
以上几种方式是在升级 alauda-ui 的过程中总结的几种方式,可以看到升级的过程并没有想象中困难,这也是为什么
Angular
自身升级 TS 4.0 相对之前迅速了很多,这可能也侧面说明了[[Define]]
语义可能并非真正的洪水猛兽。总结
class-fields
提案目前依然饱受争议,但进入规范几乎已成定局,作为开发者只能积极地拥抱变化,而从 TypeScript 4.0 升级后新特性带来的修复经验来看,只要有合适的工具来帮助我们定位这些『不符合预期』的代码,修复起来也并不费劲,但是我还是想贴一下另一位对[[Defined]]
语义不满的用户的评论。这很好地诠释了很多人对
[[Define]]
语义恐惧的原因,因为我们无法确定它是否会被终端用户覆盖掉,而 TypeScript 4.0 对这种使用方式的禁用提升了代码的可信度,或许对于纯 js 我们也可以有类似的eslint
规则帮助我们规避非预期的覆盖行为,毕竟我们已经没有办法阻止[[Define]]
语义的推进。本文首发于 知乎专栏 - 1stG 全栈之路
The text was updated successfully, but these errors were encountered: