类的装饰器

随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。TypeScript里已做为一项实验性特性予以支持。

注意:装饰器是一项实验性特性,在未来的版本中可能会发生改变。

要启用实验性的装饰器特性,必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项:

命令行:

1
tsc --target ES5 --experimentalDecorators

tsconfig.json:

1
2
3
4
"compilerOptions": {
"experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
"emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
}

装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明方法访问符属性参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

  • 类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
  • 如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

例如,有一个@test装饰器,可以这样定义test函数:

1
2
3
function test(constructor: any) {
console.log(constructor);
}

装饰器组合

多个装饰器可以同时应用到一个声明上:

1
@f @g x

当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合fg时,复合的结果(fg)(x)等同于f(g(x)),也就是按照g(x)>f(x)的顺序执行。

装饰器工厂

如果我们要定制一个修饰器如何应用到一个声明上,我们得写一个装饰器工厂函数。 装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。

可以通过以下方式定义一个装饰器工厂函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function test1(show: boolean) {
if (show) {
return function (target: any) {
name = 'change neo';
target.prototype.getName = () => {
console.log('getName');
}
}
} else {
return function (constructor: any) { }
}
}

// 通过类似注解的方式使用装饰器
@test1(true)
class UseDecorator {
name: string;
constructor(name: string) {
this.name = name;
console.log(this.name);
}
}

const useDecorator = new UseDecorator('neo')
console.log((useDecorator as any).getName()) // change neo
PS:上面的方式定义装饰器后使用装饰器内定义的getName方法,ts会提示找不到,只能通过断言将useDecorator断言为any类型后使用

下面提供一种ts能够识别装饰器中方法的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
* @Description: 装饰器
* @Author: xiuji
* @Date: 2023-08-15 14:34:59
* @LastEditTime: 2023-08-15 16:01:24
* @LastEditors: Do not edit
*/

function test1() {
// <T extends new (...args: any[]) => any>(target: T)定义一个泛型函数,入参是原有类construcor构造函数中的所有属性,返回值是一个类
return function <T extends new (...args: any[]) => any>(target: T) {
return class extends target {
name = 'neo'
getName() {
return this.name
}
}
}
}

const Test = test1()(
class UseDecorator {
name: string;
constructor(name: string) {
this.name = name;
console.log(this.name);
}
}
)

console.log(new Test('test').getName()); // neo

方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文类中。

  • 第一个参数: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 第二个参数: 是属性的名称
  • 第三个参数: 是方法的描述修饰方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @Description: 方法装饰器
* @Author: xiuji
* @param {any} target // 静态类就是构造函数,实例类就是原型对象
* @param {string} key // 方法名
* @param {PropertyDescriptor} descriptor // 属性描述符,包含value, writable, enumerable, configurable
* @return {*}
* @Date: 2023-08-16 10:37:35
*/
function getNameDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.writable = false; // 禁止在外部重写getName方法
descriptor.value = function () { // 重写getName方法
return 'decorator';
}
descriptor.enumerable = false; // 禁止在for in中遍历
descriptor.configurable = false; // 禁止删除
}


class UseDecorator {
name: string;
constructor(name: string) {
this.name = name;
}
@getNameDecorator
getName() {
return this.name;
}
}

const useDecorator = new UseDecorator('neo');
console.log(useDecorator.getName()); // decorator

访问器的装饰器

访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文类里。

  • 第一个参数: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 第二个参数: 是属性的名称
  • 第三个参数: 是方法的描述修饰方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @Description: 访问器装饰器
* @Author: xiuji
* @param {any} target // 静态类就是构造函数,实例类就是原型对象
* @param {string} key // 方法名
* @param {PropertyDescriptor} descriptor // 属性描述符,包含value, writable, enumerable, configurable
* @return {*}
* @Date: 2023-08-16 10:37:35
*/
function getNameDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
// descriptor.writable = false; // 禁止在外部重写getName方法
// descriptor.value = function () { // 重写getName方法
// return 'decorator';
// }
// descriptor.enumerable = false; // 禁止在for in中遍历
// descriptor.configurable = false; // 禁止删除
}

class UseDecorator {
private _name: string;
constructor(name: string) {
this._name = name;
}
@getNameDecorator // 访问器装饰器
get name() {
return this._name;
}
set name(name: string) {
this._name = name;
}
}

const useDecorator = new UseDecorator('neo');
useDecorator.name = 'decorator'; // 走set方法
console.log(useDecorator.name); // decorator 走get方法
注意TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个*属性描述符*时,它联合了get和set访问器,而不是分开声明的。

属性装饰器

属性装饰器声明在一个属性声明之前(紧靠着属性声明)。

  • 第一个参数: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 第二个参数: 是属性的名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @Description: 属性装饰器,不能直接修改实例上的属性,只能修改原型上的属性
* @Author:
* @param {any} target // 静态类就是构造函数,实例类就是原型对象
* @param {string} key // 方法名
* @return {any}
* @Date: 2023-08-16 10:37:35
*/

function nameDecorator(target: any, key: string): any {
console.log(target); // __proto__ 原型对象
target[key] = 'neo1'; // 修改的是prototype上的name,不是实例上的name
}

class UseDecorator {
@nameDecorator
name = 'neo';
}

const useDecorator = new UseDecorator();
console.log(useDecorator.name); // neo
console.log((useDecorator as any).__proto__.name); // neo1

参数装饰器

参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文类里。

  • 第一个参数: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 第二个参数: 成员的名字
  • 第三个参数: 参数在函数参数列表中的索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @Description: 参数装饰器
* @param {any} target // 静态类就是构造函数,实例类就是原型对象
* @param {string} method // 方法名
* @param {number} paramIndex // 参数索引
* @return {*}
* @Date: 2023-08-18 10:11:34
*/
function paramDecorator(target: any, method: string, paramIndex: number): any {
console.log(target, method, paramIndex);
}

class UseDecorator {
getName(@paramDecorator name: string, age: number) {
console.log(name, age);
}
}

const useDecorator = new UseDecorator();
useDecorator.getName('neo', 18); // neo 18
注意  参数装饰器只能用来监视一个方法的参数是否被传入。

装饰器的执行顺序

  1. 属性装饰器先执行,从上往下,谁先写先执行谁
  2. 有方法装饰器有参数装饰器时,先执行参数装饰器,从方法的最后一个参数开始,逐个向前执行。
  3. 方法装饰器,从方法的最后一个开始,逐个向前执行
  4. 类装饰器从上到下执行

下面是个装饰器执行顺序示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/*
* @Description:
* @Author: xiuji
* @Date: 2023-08-23 14:37:18`
* @LastEditTime: 2023-08-23 14:52:54
* @LastEditors: Do not edit
*/
function classDecorator1(target: any) {
console.log('类装饰器 1 executed');
}

function classDecorator2(target: any) {
console.log('类装饰器 2 executed');
}

function methodDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
console.log(`方法装饰器 for ${key} executed`);
}

function methodDecorator1(target: any, key: string, descriptor: PropertyDescriptor) {
console.log(`方法装饰器1 for ${key} executed`);
}

function propertyDecorator(target: any, key: string) {
console.log(`属性装饰器 for ${key} executed`);
}

function parameterDecorator(target: any, key: string, index: number) {
console.log(`参数装饰器 for ${key}, parameter index: ${index} executed`);
}

@classDecorator1
@classDecorator2
class ExampleClass {
@propertyDecorator
property: string;
@propertyDecorator
property1: string;

constructor() {
this.property = '示例属性';
this.property1 = '示例属性1';
}

@methodDecorator
@methodDecorator1
exampleMethod(@parameterDecorator parameter: string, @parameterDecorator parameter1: string) {
console.log('exampleMethod方法执行');
}
}

const exampleInstance = new ExampleClass();
exampleInstance.exampleMethod('参数1', '参数2');

输出结果为:

1
2
3
4
5
6
7
8
9
属性装饰器 for property executed
属性装饰器 for property1 executed
参数装饰器 for exampleMethod, parameter index: 1 executed
参数装饰器 for exampleMethod, parameter index: 0 executed
方法装饰器1 for exampleMethod executed
方法装饰器 for exampleMethod executed
类装饰器 2 executed
类装饰器 1 executed
exampleMethod方法执行 ————类构建完成,执行类中方法

装饰器使用场景示例

捕获变量中不包含对应属性时的异常信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const userInfo:any = undefined;
class UseDecorator {
getName() {
try {
return useInfo.name;
} catch (error) {
console.log('useInfo.name undefined');
}
}
getAge() {
try {
return useInfo.age;
} catch (error) {
console.log('useInfo.age undefined');
}
}
}

const useDecorator = new UseDecorator();
useDecorator.getName(); // useInfo.name undefined
useDecorator.getName(); // useInfo.age undefined

这样多个方法需要补货异常时,代码会冗余,可以通过方法装饰器统一补货异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const useInfo: any = undefined;

function catchError(msg: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const fn = descriptor.value;
descriptor.value = function () {
try {
fn();
} catch (error) {
console.log(msg);
}
}
}
}

class UseDecorator {
@catchError('useInfo.name 不存在')
getName() {
return useInfo.name;
}
@catchError('useInfo.age 不存在')
getAge() {
return useInfo.age;
}
}

const useDecorator = new UseDecorator();
useDecorator.getName(); // useInfo.name 不存在
useDecorator.getAge(); // useInfo.age 不存在