最近在学 nestjs
,发现里面有很多装饰器,实现依赖注入的效果
作为前端开发,基本不会用到装饰器,所以刚开始学习时,对装饰器的作用就不了解,决定学习一下它
装饰器
装饰器是 ts
的一个特性,可以在不改变原有代码的情况下,给类、方法、属性等添加一些额外的功能。
装饰器的本质是一个函数,它运行的阶段是在类定义的时候,而不是类实例化的时候
我们来看一下怎么编写一个装饰器
首先需要在 tsconfig.json
中开启 experimentalDecorators
选项
json
{
"compilerOptions": {
"experimentalDecorators": true
}
}
我们定义一个 log
函数,这个函数接受三个参数:
-
target
:被装饰方法所属的类 -
key
:被装饰方法的名称 -
descriptor
:被装饰方法的描述符js{ value: [Function: sayHello], // 函数本身 writable: true, // 是否可写 enumerable: false, // 是否可枚举 configurable: true // 是否可配置 }
我们定义 log
函数后,在需要使用装饰器的上面,使用 @
操作符,就可以了
如下所示:
ts
function log(target: Function, key: string, descriptor: PropertyDescriptor) {
// 拿到 sayHello 函数
const fn = descriptor.value;
// 重写 sayHello 函数
descriptor.value = function (...args: any[]) {
console.log("log");
// 调用原来的 sayHello 函数
return fn.apply(this, args);
};
}
class A {
@log
static sayHello(a: number, b: number) {
return a + b;
}
}
const a = A.sayHello(1, 2);
console.log(a);
// 🔽
// log
// 3
装饰器类型
装饰器分为五种类型:
- 类装饰器
- 属性装饰器
- 方法装饰器
- 参数装饰器
- 访问器装饰器
不同类型的装饰器,接收的类型不一样
类装饰器
类装饰器只接收一个参数 target
,target
是被装饰的类本身
ts
function d1(target: Function) {
console.log(target);
}
@d1
class B {}
属性装饰器
属性装饰器分文两种:
- 静态属性装饰器
- 实例属性装饰器
他们都是接收两个参数:
target
,如果装饰的是静态属性,那么target
就是类本身,如果装饰的是实例属性,那么target
就是类的实例key
,被装饰的属性名称
他们的执行顺序是先执行实例属性的装饰器,在执行静态属性的装饰器
ts
function d2(target: Function | Object, key: string) {
console.log("d2", target, key);
}
class B {
@d2
a: number = 1;
@d2
static b: number = 2;
}
方法装饰器
方式装饰器也是分为两种
- 静态方法装饰器
- 实例方法装饰器
他们都是接收三个参数:
-
target
,如果装饰的是静态方法,那么target
就是类本身,如果装饰的是实例方法,那么target
就是类的实例 -
key
,被装饰的方法名称 -
descriptor
,被装饰的方法的描述符js{ value: [Function: sayHello], // 函数本身 writable: true, // 是否可写 enumerable: false, // 是否可枚举 configurable: true // 是否可配置 }
它们的执行顺序也是先执行实例方法的装饰器,再执行静态方法的装饰器
js
function d3(target: Function | Object, key: string, descriptor: PropertyDescriptor) {
console.log("d3", target, key);
}
class B {
@d3
getA() {}
@d3
static getB() {}
}
参数装饰器
参数装饰器只有一种,毕竟参数是没有静态和实例之分的
它也是接收三个参数:
target
:类的实例key
:参数所在方法的名称index
:参数的索引
它们的执行顺序是先执行最后的参数装饰器,再执行前面的参数装饰器
ts
function d4(target: Function | Object, key: string, index: number) {
console.log("d4", target, key, index);
}
class B {
getA(@d4 param1: number, @d4 param2: number) {}
}
访问器装饰器
访问器装饰器也分为两种:
- 静态访问器装饰器
- 实例访问器装饰器
他们接收的阐述跟方法装饰器一致,都是接收三个参数
-
target
,如果装饰的是静态方法,那么target
就是类本身,如果装饰的是实例方法,那么target
就是类的实例 -
key
,被装饰的方法名称 -
descriptor
,被装饰的方法的描述符js{ value: [Function: sayHello], // 函数本身 writable: true, // 是否可写 enumerable: false, // 是否可枚举 configurable: true // 是否可配置 }
它们的执行顺序也是先执行实例访问器的装饰器,再执行静态访问器的装饰器
js
function d5(target: Function | Object, key: string, descriptor: PropertyDescriptor) {
console.log("d5", target, key);
}
class B {
@d5
get c() {
return 1;
}
@d5
static get c() {
return 1;
}
}
装饰器的执行顺序
实例装饰器 => 静态装饰器 => 类装饰器
具体的过程如下:
- 参数装饰器在方法装饰器之前执行
- 参数装饰器从后往前执行
- 实例装饰器在静态装饰器之前执行
- 写在前面的装饰器先执行
- 类装饰器总是最后执行
装饰器工厂函数
装饰器工厂函数的意思是,装饰器可以接收一个参数,并返回一个函数,返回的函数就是遵循装饰器类型
通过传入一个参数就可以实现对同一个装饰器处理不同的逻辑
代码如下:
js
function log(type: string) {
return function (target: Function, key: string, descriptor: PropertyDescriptor) {
const fn = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log("log", type);
return fn.apply(this, args);
};
};
}
class A {
@log("type1")
static sayHello(a: number, b: number) {
return a + b;
}
@log("type2")
static sayHello2(a: number, b: number) {
return a + b;
}
}
const a = A.sayHello(1, 2);
console.log(a);
const b = A.sayHello2(11, 22);
console.log(b);
元数据
我们先来看一个需求
如果我们一个类中要有多个方法要使用 log("type1")
装饰器,那么我们就需要在每个方法上都写一遍,这样就会很麻烦
有没有什么方法可以让我们只写一遍呢?
就是在类装饰器上传入一个参数,因为 target
是类本身,所以我们在它的原型上,也就是 prototype
上挂一个属性 type
当方法装饰器执行时,去读取 prototype
上的 type
属性就可以了
js
function l(type: string) {
return function (target: Function) {
// 将 type 挂载到原型上
target.prototype.type = type;
};
}
function log(type?: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const fn = descriptor.value;
descriptor.value = function (...args: any[]) {
let _type = type;
if (!_type) {
// 静态方法的 target 是类本身,类本质是一个函数,通过判断 target 是否是函数,来判断是静态方法还是实例方法
// 如果是静态方法,那么要取到 type,就要通过 prototype 来取
// 否则就是实例方法,直接取 type 就可以了
if (typeof target === "function") {
_type = target.prototype.type;
} else {
_type = target.type;
}
}
console.log("log", _type);
return fn.apply(this, args);
};
};
}
@l("type1")
class A {
@log()
static sayHello(a: number, b: number) {
return a + b;
}
@log("type222")
static sayHello2(a: number, b: number) {
return a + b;
}
}
const a = A.sayHello(1, 2);
console.log(a);
const b = A.sayHello2(11, 22);
console.log(b);
这种方法你在原型上定义了一个属性 type
但是就会出现一个问题,如果有一个人不知道,和你定义了同样一个属性名,这时你的 type
就有可能会被覆盖
这种危险的行为是不允许的,这时就出现了元数据,我们用元数据来解决这个问题
什么是元数据呢?
元数据是用来描述数据的数据,在我们的对象中:类,对象都是数据,它们描述了某种数据
那如何描述类和对象呢?
这个就是元数据
举个例子,我们用类或者对象来描述一个人,那么我们会用元数据描述这个类或者对象应该有哪些属性
我们使用第三方库 reflect-metadata
reflect-metadata
这个库的作用是可以给类或者对象定义一些元数据,
这些数据会被附加到指定的类或者方法之上,但是又不会影响到类或者方法本身的代码
主要介绍两个方法:
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey)
:定义元数据metadataKey
:元数据的名称metadataValue
:元数据的值target
:被装饰的类或者对象propertyKey
:被装饰的方法名称- 这里要注意
target
和propertyKey
要一一对应,如果是类,那么propertyKey
就是静态属性或者方法,如果是对象,那么propertyKey
就是实例属性或者方法
- 这里要注意
Reflect.getMetadata(metadataKey, target, propertyKey)
:获取元数据
通过 Reflect.defineMetadata
方法调用来添加元数据,通过 @Reflect.metadata
给类添加元数据
ts
import "reflect-metadata";
class A {
@Reflect.metadata("key", "3")
public static method1() {}
public method2() {}
}
let obj = new A();
Reflect.defineMetadata("key", "1", A);
Reflect.defineMetadata("key", "2", obj);
Reflect.defineMetadata("key", "3", A, "method1"); // 等价于 @Reflect.metadata("key", "3") public static method1() {}
Reflect.defineMetadata("key", "4", obj, "method2");
console.log(Reflect.getMetadata("key", A));
console.log(Reflect.getMetadata("key", obj));
console.log(Reflect.getMetadata("key", A, "method1"));
console.log(Reflect.getMetadata("key", obj, "method2"));
对上面例子的改造:
ts
function l(type: string) {
return function (target: any) {
// 将 type 挂载到类上,元数据的形式,而不是直接挂在到类上
Reflect.defineMetadata("type", type, target);
};
}
function log(type?: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const fn = descriptor.value;
descriptor.value = function (...args: any[]) {
let _type = type;
if (!_type) {
if (typeof target === "function") {
// 通过 Reflect.getMetadata 获取元数据
_type = Reflect.getMetadata("type", target);
} else {
// target.constructor 获取实例
_type = Reflect.getMetadata("type", target.constructor);
}
}
console.log("log", _type);
return fn.apply(this, args);
};
};
}
@l("type1")
class A {
@log()
static sayHello(a: number, b: number) {
return a + b;
}
@log("type222")
sayHello2(a: number, b: number) {
return a + b;
}
}
const a = A.sayHello(1, 2);
console.log(a);
const b = new A().sayHello2(11, 22);
console.log(b);
emitDecoratorMetadata
在 tsconfig.json
配置了 emitDecoratorMetadata
后,ts
会在编译后自动添加三个元数据:
design:type
- 属性:属性标注的类型
- 方法:
Function
类型
design:paramtypes
:方法的参数类型- 方法:形参标注的类型
- 类:构造函数形参标注的类型
design:returntype
:方法的返回值类型- 方法:函数返回值标志的类型
代码如下:
ts
function log(type?: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const designType = Reflect.getMetadata("design:type", target, key);
const designParamTypes = Reflect.getMetadata("design:paramtypes", target, key);
const designReturnType = Reflect.getMetadata("design:returntype", target, key);
console.log(designType, designParamTypes, designReturnType); // [Function: Function] [ [Function: String], [Function: String] ] [Function: String]
};
}
class A {
@log()
method2(a: string, b: string): string {
return a + b;
}
}
const a = new A();
const b = a.method2("1", "2");
console.log(b);
有了这三种参数后,就可以实现依赖注入了
代码如下:
js
import "reflect-metadata";
function Inject(target: any, key: string) {
target[key] = new (Reflect.getMetadata("design:type", target, key))();
}
class A {
sayHello() {
console.log("hello");
}
}
class B {
@Inject // 编译后等同于执行了 @Reflect.metadata("design:type", A)
a!: A;
say() {
this.a.sayHello(); // 不需要再对class A进行实例化
}
}
new B().say(); // hello