原文转自:https://mirone.me/zh-hans/a-complete-guide-to-typescript-decorator/
装饰器让 TypeScript 的世界更好。 我们使用的许多库都基于这一强大特性构建,例如 Angular 和 Nestjs。 在这篇博客中我将介绍装饰器和它的许多细节。 我希望在读完这篇文章后,你可以理解何时和如何使用这一强的的特性。
装饰器本质上是一种特殊的函数被应用在于:
- 类
- 类属性
- 类方法
- 类访问器
- 类方法的参数
所以应用装饰器其实很像是组合一系列函数,类似于高阶函数和类。 通过装饰器我们可以轻松实现 代理模式 来使代码更简洁以及实现其它一些更有趣的能力。
装饰器的语法十分简单,只需要在想使用的装饰器前加上 @
符号,装饰器就会被应用到目标上:
一共有 5 种装饰器可被我们使用:
- 类装饰器
- 属性装饰器
- 方法装饰器
- 访问器装饰器
- 参数装饰器
让我们来快速认识一下这五种装饰器:
时机
装饰器只在解释执行时应用一次,例如:
这里的代码会在终端中打印 apply decorator
,即便我们其实并没有使用类 A。
执行顺序
不同类型的装饰器的执行顺序是明确定义的:
- 实例成员:
参数装饰器 → 方法 / 访问器 / 属性 装饰器 2. 静态成员:
参数装饰器 → 方法 / 访问器 / 属性 装饰器 3. 构造器:参数装饰器 4. 类装饰器
例如,考虑以下代码:
它将会打印出以下信息:
你也许会注意到执行实例属性 prop
晚于实例方法 method
然而执行静态属性 static prop
早于静态方法 static method
。 这是因为对于属性 / 方法 / 访问器装饰器而言,执行顺序取决于声明它们的顺序。
然而,同一方法中不同参数的装饰器的执行顺序是相反的, 最后一个参数的装饰器会最先被执行:
这里的代码打印出的结果为:
多个装饰器的组合
你可以对同一目标应用多个装饰器。它们的组合顺序为:
- 求值外层装饰器
- 求值内层装饰器
- 调用内层装饰器
- 调用外层装饰器
例如:
这里的代码打印出的结果为:
类装饰器
类型声明:
- @参数:
target
: 类的构造器。
- @返回:
如果类装饰器返回了一个值,她将会被用来代替原有的类构造器的声明。
因此,类装饰器适合用于继承一个现有类并添加一些属性和方法。
例如我们可以添加一个 toString
方法给所有的类来覆盖它原有的 toString
方法。
遗憾的是装饰器并没有类型保护,这意味着:
这是 一个 TypeScript 的已知的缺陷。 目前我们能做的只有额外提供一个类用于提供类型信息:
属性装饰器
类型声明:
- @参数:
target
: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
propertyKey
: 属性的名称。
- @返回:
返回的结果将被忽略。
除了用于收集信息外,属性装饰器也可以用来给类添加额外的方法和属性。 例如我们可以写一个装饰器来给某些属性添加监听器。
方法装饰器
类型声明:
- @参数:
target
: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
propertyKey
: 属性的名称。
descriptor
: 属性的 描述器。
- @返回: 如果返回了值,它会被用于替代属性的描述器。
方法装饰器不同于属性装饰器的地方在于 descriptor
参数。 通过这个参数我们可以修改方法原本的实现,添加一些共用逻辑。 例如我们可以给一些方法添加打印输入与输出的能力:
访问器装饰器
访问器装饰器总体上讲和方法装饰器很接近,唯一的区别在于描述器中有的 key 不同:
方法装饰器的描述器的 key 为:
value
writable
enumerable
configurable
访问器装饰器的描述器的 key 为:
get
set
enumerable
configurable
例如,我们可以将某个属性设为不可变值:
参数装饰器
类型声明:
- @参数:
target
: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
propertyKey
: 属性的名称 (注意是方法的名称,而不是参数的名称)。
parameterIndex
: 参数在方法中所处的位置的下标。
- @返回:
返回的值将会被忽略。
单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。
对于一些复杂场景, 我们可能需要结合使用不同的装饰器。 例如如果我们不仅想给我们的接口添加静态检查,还想加上运行时检查的能力。
我们可以用 3 个步骤来实现这个功能:
- 标记需要检查的参数 (因为参数装饰器先于方法装饰器执行)。
- 改变方法的
descriptor
的 value
的值,先运行参数检查器,如果失败就抛出异常。
- 运行原有的接口实现。
以下是代码:
正如例子中展示的, 对我们来说同时理解不同种类装饰器的执行顺序和职责都很重要。
严格地说,元数据和装饰器是 EcmaScript 中两个独立的部分。 然而,如果你想实现像是 反射 这样的能力,你总是同时需要它们。
如果我们回顾上一个例子,如果我们不想写各种不同的检查器呢? 或者说,能否只写一个检查器能够通过我们编写的 TS 类型声明来自动运行类型检查?
有了 reflect-metadata 的帮助, 我们可以获取编译期的类型。
目前为止一共有三种编译期类型可以拿到:
design:type
: 属性的类型。
desin:paramtypes
: 方法的参数的类型。
design:returntype
: 方法的返回值的类型。
这三种方式拿到的结果都是构造函数(例如 String
和 Number
)。规则是:
- number →
Number
- string →
String
- boolean →
Boolean
- void/null/never →
undefined
- Array/Tuple →
Array
- Class → 类的构造函数
- Enum → 如果是纯数字枚举则为
Number
, 否则是 Object
- Function →
Function
- 其余都是
Object
现在我们可以对于何时使用装饰器得出结论, 在阅读上面的代码中你可能也有所感觉。
我将例举一些常用的使用场景:
- Before/After 钩子。
- 监听属性改变或者方法调用。
- 对方法的参数做转换。
- 添加额外的方法和属性。
- 运行时类型检查。
- 自动编解码。
- 依赖注入。
我希望读完这篇文章后,你可以找到装饰器的更多使用场景,并且用它来简化你的代码。