TypeScript 中的装饰器

玩了一下装饰器(decorator)。这是个好东西,但我还是没法实现 @Sealed 啊。


最近看到 BiliBili 开了个 BAS,新的高级弹幕语言,于是启动了 SEBAS。不过想略微吐槽一下,功能只是 Mode 8 子集的子集,惨不忍睹。安全是安全了,不过再也做不出像3D球那样有趣的玩意儿了。另外,确定这语法不是拍脑袋啊想出来的?

闲话少说,进入正题。在此之前,各位要先有点知识储备。当然如果了解 Python 的装饰器函数也行。不要吐槽为什么 TypeScript 网站的文档要把导航放最上面而且还要占用这么大尺寸。

装饰器可以对元数据进行修饰。这和装饰模式不同,并不是以包装的方式来实现扩展的。对于 Python 和 JavaScript 这种可以随便往对象上面挂成员(Ruby 可以开包,不过这个我不熟),而且函数是一等公民的语言,元数据和代码在形式上区别并不大。特性和注解是被动地存储信息,对成员的改动需要通过其他函数进行(或者编译时,比如辅助 AOP 的特性),行为要通过包装类改变;而对于 Python 和 JavaScript 而言,装饰器既承担了保存额外信息的功能,又负责对成员作改动。稍微花点时间适应一下这个变化。

好了该讲讲我为什么突然玩了玩这个玩意儿。

各位都知道,受到“类”在 JavaScript 中实现的限制,所有的实例方法都是像 Java 一样是默认覆盖(override)的。Java 后来引入了 @Override 注解,编译器可以帮助检查这个函数是否本来就是想覆盖的,以及基类型是否有待覆盖的方法。但是 JavaScript 没有,也没法检查(因为本身就没有继承),是完全动态的。我在编写 SEBAS 的时候,希望不要因为打错字而造成编译没错、运行出错(找不到成员)的事情,所以希望加入这样一个检查。所以我实现了 @Override 装饰器,进行运行时检查。是的,不是编译时检查,因为 JavaScript(在执行引擎之外)没有“编译”的概念。不过每个方法只需要在引入时检查一遍,所以应该还是可以接受的。

我们先来看一下核心逻辑。什么时候应该判断为合法?应用了 @Override 的方法覆盖了基类的同名方法。什么时候该判断为不合法?一个方法应用了 @Override,但无法在基类型中找到同名方法。至于没有应用 @Override 的方法,受到条件限制我们就管不着了。函数签名也管不着(Function.length 不能代表一切)。这就好办了,沿着原型链一直往上找就行了。

但是还有问题。TypeScript 中是可以定义抽象类的,有着“不可实例化”的语义。而在转译(transpile)到 JavaScript 之后,就变成了普通的函数,丢失了“不可实例化”的语义。在 TypeScript 中检查还是比较简单的,毕竟是单继承,一个是不可实例化,一个是 super.method() 不可访问(其中 super 指向一个抽象基类),这只需要考察一层继承。但是就装饰器所执行的 JavaScript 来说,丢失的东西太多了。所以还需要有一个 @Abstract 装饰器,表明这个类是抽象类。当分析到这个抽象类时,乐观假设指定的方法是存在的。也就是说,@Abstract 的设计是为了解决这种问题:

1
2
3
4
5
6
7
8
9
@Abstract
abstract class A {
abstract method(): void;
}

class B extends A {
@Override()
method():void { }
}

如果没有了 @Abstract,那么对 method 的检查是肯定要报错的。

至于我没法实现的 @Seal,我的设想是与主流语言的语义保持一致:类型封闭,不允许添加和删除,不允许继承。这个行为介于 Object.freeze()Object.seal() 之间,没有标准函数定义这个状态。而且关键的“不允许继承”是根本做不到的——访问原型是一直允许的,将一个函数的原型指向已有的原型也是一直允许的,但我们无法操控用户所生成的函数。因此继承是永远不可禁止的。哎,遗憾。

分享到 评论