虽然 JavaScript 目前还没有装饰器的概念(ES6 类的装饰器提案[1]目前处于第三阶段,还未完成),但 TypeScript 5.0 中已经引入了装饰器,本文将了解什么是装饰器以及如何在 TypeScript 中使用装饰器!
1
装饰器的概念
Summer IS HERE
在 TypeScript 中,装饰器就是可以添加到类及其成员的函数。TypeScript 装饰器可以注释和修改类声明、方法、属性和访问器。Decorator
类型定义如下:
type Decorator = (target: Input, context: {
kind: string;
name: string | symbol;
access: {
get?(): unknown;
set?(value: unknown): void;
};
private?: boolean;
static?: boolean;
addInitializer?(initializer: () => void): void;
}) => Output | void;
上面的类型定义解释如下:
target
:代表要装饰的元素,其类型为 Input
context
包含有关如何声明修饰方法的元数据,即:
kind
:装饰值的类型。正如我们将看到的,这可以是类、方法、getter
、setter
、字段或访问器
name
:被装饰对象的名称
access
:引用 getter 和 setter 方法来访问装饰对象的对象
private
:被装饰的对象是否是私有类成员
static
:被修饰的对象是否是静态类成员
addInitializer
:一种在构造函数开头(或定义类时)添加自定义初始化逻辑的方法
Output
:表示 Decorator
函数返回值的类型
2
装饰器的类型
Summer IS HERE
接下来,我们就来了解一下装饰器的各种类型。
Summer
类装饰器
当将函数作为装饰器附加到类时,将收到类构造函数作为第一个参数:
type ClassDecorator = (value: Function, context: {
kind: "class"
name: string | undefined
addInitializer(initializer: () => void): void
}) => Function | void
例如,假设想要使用装饰器向 Rocket
类添加两个属性:fuel
和 isEmpty()
。在这种情况下,可以编写以下函数:
function WithFuel(target: typeof Rocket, context): typeof Rocket {
if (context.kind === "class") {
return class extends target {
fuel: number = 50
isEmpty(): boolean {
return this.fuel == 0
}
}
}
}
在确保装饰元素的类型确实是类之后,返回一个具有两个附加属性的新类。或者,可以使用原型对象来动态添加新方法:
function WithFuel(target: typeof Rocket, context): typeof Rocket {
if (context.kind === "class") {
target.prototype.fuel = 50
target.prototype.isEmpty = (): boolean => {
return this.fuel == 0
}
}
}
可以按以下方式使用 WithFuel
:
@WithFuel
class Rocket {}
const rocket = new Rocket()
console.log((rocket as any).fuel)
console.log(`empty? ${(rocket as any).isEmpty()}`)
/* Prints:
50
empty? false
*/
可以看到,这里将rocket
转换为any
类型才能访问新的属性。这是因为装饰器无法影响类型的结构。
如果原始类定义了一个稍后被装饰的属性,装饰器会覆盖原始值。例如,如果Rocket
有一个具有不同值的fuel
属性,WithFuel
装饰器将会覆盖该值:
function WithFuel(target: typeof Rocket, context): typeof Rocket {
if (context.kind === "class") {
return class extends target {
fuel: number = 50
isEmpty(): boolean {
return this.fuel == 0
}
}
}
}
@WithFuel
class Rocket {
fuel: number = 75
}
const rocket = new Rocket()
console.log((rocket as any).fuel)
// 50
Summer
方法装饰器
方法装饰器可以用于装饰类方法。在这种情况下,装饰器函数的类型如下:
type ClassMethodDecorator = (target: Function, context: {
kind: "method"
name: string | symbol
access: { get(): unknown }
static: boolean
private: boolean
addInitializer(initializer: () => void): void
}) => Function | void
如果希望在调用被装饰的方法之前或之后执行某些操作时,就可以使用方法装饰器。
例如,在开发过程中,记录对特定方法的调用或在调用之前/之后验证前置/后置条件可能非常有用。此外,我们还可以影响方法的调用方式,例如通过延迟其执行或限制在一定时间内的调用次数。
最后,可以使用方法装饰器将一个方法标记为已废弃,并记录一条消息来警告用户,并告知他们应该使用哪个方法代替:
function deprecatedMethod(target: Function, context) {
if (context.kind === "method") {
return function (...args: any[]) {
console.log(`${context.name} is deprecated and will be removed in a future version.`)
return target.apply(this, args)
}
}
}
在这种情况下,deprecatedMethod
函数的第一个参数是要装饰的方法。确认它确实是一个方法后(context.kind === "method"
),返回一个新的函数,该函数在调用实际方法之前包装被装饰的方法并记录一条警告消息。
接下来,可以按照以下方式使用装饰器:
@WithFuel
class Rocket {
fuel: number = 75
@deprecatedMethod
isReadyForLaunch(): Boolean {
return !(this as any).isEmpty()
}
}
const rocket = new Rocket()
console.log(`Is ready for launch? ${rocket.isReadyForLaunch()}`)
在isReadyForLaunch()方法中,引用了通过WithFuel
装饰器添加的isEmpty
方法。注意,必须将其转换为any
类型的实例,与之前一样。当调用isReadyForLaunch()
方法时,会看到以下输出,显示警告消息被正确地打印出来:
isReadyForLaunch is deprecated and will be removed in a future version.
Is the ready for launch? true
Summer
属性装饰器
属性装饰器与方法装饰器的类型非常相似:
type ClassPropertyDecorator = (target: undefined, context: {
kind: "field"
name: string | symbol
access: { get(): unknown, set(value: unknown): void }
static: boolean
private: boolean
}) => (initialValue: unknown) => unknown | void
属性装饰器的用例与方法装饰器的用法也非常相似。例如,可以跟踪对属性的访问或将其标记为已弃用:
function deprecatedProperty(_: any, context) {
if (context.kind === "field") {
return function (initialValue: any) {
console.log(`${context.name} is deprecated and will be removed in a future version.`)
return initialValue
}
}
}
代码与为方法定义的 deprecatedMethod
装饰器非常相似,它的用法也是如此。
Summer
访问器装饰器
与方法装饰器非常相似的是访问器装饰器,它是针对 getter
和 setter
的装饰器:
type ClassSetterDecorator = (target: Function, context: {
kind: "setter"
name: string | symbol
access: { set(value: unknown): void }
static: boolean
private: boolean
addInitializer(initializer: () => void): void
}) => Function | void
type ClassGetterDecorator = (value: Function, context: {
kind: "getter"
name: string | symbol
access: { get(): unknown }
static: boolean
private: boolean
addInitializer(initializer: () => void): void
}) => Function | void
访问器装饰器的定义与方法装饰器的定义类似。例如,可以将 deprecatedMethod
和 deprecatedProperty
修饰合并到一个已弃用的函数中,该函数也支持 getter
和 setter
:
function deprecated(target, context) {
const kind = context.kind
const msg = `${context.name} is deprecated and will be removed in a future version.`
if (kind === "method" || kind === "getter" || kind === "setter") {
return function (...args: any[]) {
console.log(msg)
return target.apply(this, args)
}
} else if (kind === "field") {
return function (initialValue: any) {
console.log(msg)
return initialValue
}
}
}
3
装饰器的用例
Summer IS HERE
上面介绍了装饰器是什么以及如何正确使用它们,下面来看看装饰器可以帮助我们解决的一些具体问题。
Summer
计算执行时间
假设想要估计运行一个函数需要多长时间,以此来衡量应用的性能。可以创建一个装饰器来计算方法的执行时间并将其打印在控制台上:
class Rocket {
@measure
launch() {
console.log("3... 2... 1... ");
}
}
Rocket 类内部有一个 launch
方法。要测量launch
方法的执行时间,可以附加measure
装饰器:
import { performance } from "perf_hooks";
function measure(target: Function, context) {
if (context.kind === "method") {
return function (...args: any[]) {
const start = performance.now()
const result = target.apply(this, args)
const end = performance.now()
console.log(`Time: ${end - start} s`)
return result
}
}
}
可以看到,measure
装饰器会替换原始方法,并使用新方法来计算原始方法的执行时间并将其打印到控制台。为了计算执行时间,可以使用 Node.js 标准库中的性能钩子(Performance Hooks)API。实例化一个新的Rocket
对象并调用launch
方法:
const rocket = new Rocket()
rocket.launch()
将得到以下结果:
3... 2... 1...
Time: 1.062355000525713 s
Summer
使用装饰器工厂函数
要将装饰器配置为在特定场景中采取不同的行为,可以使用装饰器工厂。装饰器工厂是返回装饰器的函数。这样就能够通过在工厂中传递一些参数来自定义装饰器的行为。
来看下面的例子:
function fill(value: number) {
return function(_, context) {
if (context.kind === "field") {
return function (initialValue: number) {
return value + initialValue
}
}
}
}
fill
函数返回一个装饰器,根据从工厂传入的值来改变属性的值:
class Rocket {
@fill(20)
fuel: number = 50
}
const rocket = new Rocket()
console.log(rocket.fuel) // 70
Summer
自动错误拦截
装饰器的另一个常见用例是检查方法调用的前置条件和后置条件。例如,假设要在调用 launch()
方法之前确保 Fuel
至少为给定值:
class Rocket {
fuel = 50
launch() {
console.log("3... 2... 1... ")
}
}
假设有一个 Rocket
类,它有一个 launchToMars
方法。要发射火箭,燃料(fuel
)必须高于一个值,例如 75。
下面来为它创建装饰器:
function minimumFuel(fuel: number) {
return function(target: Function, context) {
if (context.kind === "method") {
return function (...args: any[]) {
if (this.fuel > fuel) {
return target.apply(this, args)
} else {
console.log(`Not enough fuel. Required: ${fuel}, got ${this.fuel}`)
}
}
}
}
}
minimumFuel
是一个工厂装饰器。它接受一个 fuel
参数,表示启动特定火箭所需的燃料量。为了检查燃料条件,将原始方法包裹在一个新方法中。注意,在运行时可以自由地引用 this.fuel
。
现在就可以将装饰器应用到launch
方法上,并设置最低燃料量:
class Rocket {
fuel = 50
@minimumFuel(75)
launch() {
console.log("3... 2... 1... ")
}
}
如果现在调用 launch
方法,它不会发射火箭,因为当前的燃料量为 50:
const rocket = new Rocket()
rocket.launch()
Not enough fuel. Required: 75, got 50
[1]装饰器提案: https://github.com/tc39/proposal-decorators
Summer
往期推荐
10个超炫酷的前端3D开源项目
2023 年的 Node.js 生态系统
深入理解 JSX:从零开始实现一个 JSX 解析器
70k Star 的 Tailwind CSS 有什么特别之处?拯救你的 CSS 开发!
如何在页面上优雅的展示代码?
前端新人入职必备清单,保姆级教程!
- END -
奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。