目前 decorator 草案仍然处于 stage2 阶段,将来 API 可能会有破坏性改动,所以本文内容以目前的 ts 中的 decorator 定义为主。
装饰器的功能
装饰器是一个函数,可使用类声明,方法, 访问符,属性或参数上,被装饰的对象会作为参数传入。在装饰器中可以对目标(target)进行一些操作。
假设有个User
类,里面有个eat
方法代表吃饭。假设有一天需要日志记录吃饭情况,于是在方法中里面加入日志代码。
1 | class User { |
后来又需要记录睡觉(sleep)的情况,则在 sleep 方法写入相似的代码。
1 | class User { |
对于User
来说,他只是会执行吃饭和睡觉的动作,而日志记录并不是他自身关注点,这样违反了单一职责的原则,同时写大量的 log 代码无疑是非常冗余的。
通过装饰器,我们可以将日志记录进行统一封装,然后“装饰”在需要记录的方法上面,方法内部不需要任何改动就可以实现日志记录的功能。
1 | // logger是一个装饰器工厂 |
由于装饰器是声明式的写法,不会对原来代码进行侵入式的修改。如果要移除此功能只需要删除装饰器即可,是不是很方便?
装饰器组合
装饰器可以多个组合使用
1 | const f = () => { |
输出如下
1 | f() |
这个执行顺序是不是有点眼熟? 如果你用过函数组合就能看出和 compose(f(),g())(Foo)
是相同的执行顺序。
此外,针对在类的不同位置声明的装饰器,执行顺序有如下规则。
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
- 参数装饰器应用到构造函数。
- 类装饰器应用到类。
装饰器类型
类装饰器
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。如果类装饰器返回了一个新的值,而新的值会替换原有的构造函数。
从类装饰器的实现上看,传入一个类返回一个新的值,如果这个值也是一个类,是不是和 react 的高阶组件很像呢?
1 | const wrapComp = Comp => { |
所以高阶组件本质上也是一个装饰器,可以用装饰器的方法进行使用。
1 | ( |
等价于
1 | class SomeComp extends React.Component { |
方法装饰器
方法装饰器表达式会在运行时当作函数被调用,传入下列 3 个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 成员的属性描述符。
上面的 logger
就是一个方法装饰器。
如果方法装饰器返回一个值,它会被用作方法的属性描述符。
1 | function enumerable(value: boolean) { |
访问器装饰器
访问器装饰器和方法装饰器类似。需要注意的是不能同时给同一个属性的getter
和setter
设置装饰器。
属性装饰器
属装饰器表达式会在运行时当作函数被调用,传入下列 2 个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
参数装饰器
参数装饰器表达式会在运行时当作函数被调用,传入下列 3 个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 参数在函数参数列表中的索引。
元数据 Metadata
Metadata 目前还处于提案阶段,还不是 ecmascript 标准的一部分,但是在 typescript 中已经可以用来使用了。
定义
元数据(Metadata),又称中介数据、中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能。
既然是用来描述数据属性的信息,也就是说元数据用来修饰类本身的一些性质,和类实例没有直接关系。
我们看看元数据的语义上的存储位置:
- 对于类 C 本身,存储在
C.\[\[Metadata\]\].undefined
属性中 - 对于类 C 的静态成员,存储在
C.\[\[Metadata\]\].
属性中 - 对于类 C 的实例成员,存储在
C.proptype.\[\[Metadata\]\].
属性中
事实上也是如此吗???
1 | import 'reflect-metadata'; |
奇怪的是,运行后C['[[Metadata]]']
的值为 undefined。经过查询源码得知,ts的reflect-metadata
的实现并没有将元数据存在类本身,而是存在一个全局的WeakMap
对象上的,通过传入的target和property作为键去进行元数据的声明和查询的。
1 | // [[Metadata]] internal slot |
反射
如果使用原生 js,通过元数据反射可以实现动态(这里有区别于 ts 的静态检查)的参数校验。
1 | import 'reflect-metadata'; |
通过@Type
装饰器在元数据中存储了name
的类型,然后validate
装饰器重写了 setter 方法,在内部取得name
的类型并进行了校验。
在上面的例子中通过装饰器声明了类型,实际上 ts 本身在写代码的时候就需要声明类型,通过在tsconfig
中将emitDecoratorMetadata
设置为true
,ts 在编译时会自动的对被装饰的目标生成类型的元数据。
包括以下几个值
design:type
- 目标类型design:paramtypes
- 目标参数的类型design:returntype
- 目标的返回值
但是并不是所有类型的装饰器都会有上述的全部元数据。
1 | import 'reflect-metadata'; |
运行的结果为
1 | parameter metadata |
根据输出的结果,可以得到如下元数据的情况:
目标 | design:type | design:paramtypes | design:returntype |
---|---|---|---|
类 | ❌ | ✅ | ❌ |
方法 | ✅ | ✅ | ✅ |
属性 | ✅ | ❌ | ❌ |
参数 | ✅ | ✅ | ✅ |
从编译后的代码中,可以看到是元数据是如何生成的
1 | var People = /** @class */ (function() { |
作用
从官方的文档中能够看出元数据的使用场景
A number of use cases (Composition/Dependency Injection, Runtime Type Assertions, Reflection/Mirroring, Testing) want the ability to add additional metadata to a class in a consistent manner.
这里有一篇好友经纬的关于依赖注入的文章-基于 TypeScript 的 IoC 和 DI,值得一看。
本文就先写到这里,有什么不对的地方请指正。