前言
很多语言和方法都有 AOP 编程。AOP 的好处是只需要写一次函数检查,在函数调用前只做引用即可。极大的减少了重复代码的编写。
试想一下:在函数入参检查类型时需要反复用 typeof parameter === '类型' 来做检查时一件非常痛苦的事情。虽然用了 Typescript,但只是解决了编码时候的类型校验,而运行时的校验依旧需要编码来做检查。
本篇介绍的 Decorator 用法,就是为了解决这一困扰而出现的。它不仅一行代码解决了运行时的入参类型检查;还能用一行代码做函数权限检查,只让有权限的人调用;更能一行代码解决入参和结果的日志打印。让代码更容易维护的同时,也更专注于业务的实现。
如果您对例子感兴趣,可以直接到使用举例
啥是 Decorator?
Decorator 是 ES6 中的提案之一,它实际上是个 wrapper,可以为类、属性或函数提供额外功能。举个🌰:
1 | function f(key: string): any { |
使用前的准备
虽然 Decorator 只是一个提案,但可通过工具来使用它:
Babel:
babel-plugin-syntax-decorators
babel-plugin-transform-decorators-legacy
Typescript:
命令行:
1 | tsc --target ES5 --experimentalDecorators |
tsconfig.json:
1 | { |
定义

类装饰器
📌 参数:
target: 类的构造器(constructor)
⬅️ 返回值: undefined | 替代原有构造器
因此,类装饰器适合用于继承一个现有类并添加一些属性和方法。
1 | function rewirteClassConstructor<T extends { new (...args: any[]): {} }>(constructor: T) { |
属性装饰器
📌 参数:
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链propertyKey: 属性名称
⬅️ 返回值: 返回的结果将被忽略
除了用于收集信息外,属性装饰器也可以用来给类添加额外的方法和属性。 例如我们可以写一个装饰器来给某些属性添加监听器。
1 | import "reflect-metadata"; |
方法装饰器
📌 参数:
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链propertyKey: 属性名称descriptor: 属性的 描述器
⬅️ 返回值:undefined | 替代属性的描述器。
方法装饰器descriptor的key为:
1 | value |
通过这个参数我们可以修改方法原本的实现,添加一些共用逻辑。 例如我们可以给一些方法添加打印输入与输出的能力:
1 | function logger(target: any, propertyKey: string, descriptor: PropertyDescriptor) { |
访问器装饰器
📌 参数:
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链propertyKey: 属性名称descriptor: 属性的 描述器
⬅️ 返回值:undefined | 替代属性的描述器。
访问器装饰器descriptor的key为:
1 | get |
访问器装饰器总体上讲和方法装饰器很接近,唯一的区别在于描述器中有的key不同例如,我们可以将某个属性设为不可变值:
1 | function immutable(target: any, propertyKey: string, descriptor: PropertyDescriptor) { |
参数装饰器
📌 参数:
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链propertyKey: 属性的名称(注意是方法的名称,而不是参数的名称)paramerterIndex: 参数在方法中所处的位置的下标
⬅️ 返回值:返回的值将会被忽略。
单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。
1 | // parameter.ts |
1 | // test.js |
执行顺序
不同类型的装饰器执行顺序是明确的:
1、 实例成员:参数装饰器 -> 方法/访问器/属性 装饰器
2、 静态成员:参数装饰器 -> 方法/访问器/属性 装饰器
3、 构造函数:参数装饰器
4、 类装饰器
例如:
1 | function f(key: string): any { |
然而,在同一方法中的不同参数构造器顺序是相反的,最后参数回的装饰器会先被执行:
1 |
|
使用场景
- Before/After钩子。
- 监听属性改变或者方法调用。
- 对方法的参数做转换。
- 添加额外的方法和属性。
- 运行时类型检查。
- 自动编解码。
- 依赖注入。
使用举例
日志打印
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function f(): any {
return function (target, key, descriptor) {
let method = descriptor.value;
descriptor.value = function () {
console.log('param: ', Array.from(arguments));
const value = method.apply(this, arguments);
console.log('result: ', value);
return value
};
};
}
class B {
()
say(name: string) {
return `name is ${name}`;
}
}鉴权:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function auth(user) {
return function(target, key, descriptor) {
var originalMethod = descriptor.value; // 保留原有函数
if (!user.isAuth) {
descriptor.value = function() { // 未登录将返回提示
console.log('当前未登录,请登录!');
}
} else {
descriptor.value = function (...args) { // 已登录将原有函数
originalMethod.apply(this, args);
}
}
return descriptor;
}
}
(app.user)
function handleStar(new) {
new.like++;
}类型检查
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
36import "reflect-metadata";
const stringMetaDataTag = "IsString";
function IsString(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata(stringMetaDataTag, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata( stringMetaDataTag, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
let method = descriptor.value!;
descriptor.value = function () {
let stringMetaTags: number[] = Reflect.getOwnMetadata(stringMetaDataTag, target, propertyName);
if (stringMetaTags) {
for (let parameterIndex of stringMetaTags) {
const value = arguments[parameterIndex];
if (!(value instanceof String || typeof value === 'string')) {
throw new Error('not string');
}
}
}
return method.apply(this, arguments);
};
}
export class A {
a: string = '123';
value ( value: string) {
console.log(value);
this.a = value;
}
}
写在最后
笔者在 后台接口、Js Bridge、React 项目上都有实践过。不得不说,装饰器模式在面向切面编程(AOP)几乎是 “最佳实践”,极大的提升了编程效率。也希望这篇文章能帮助到你😊
npm 包
class-validator
core-decorators
Nest 后台框架
参考链接
tc39-proposal
typescript
a-complete-guide-to-typescript-decorator

