装饰器 Decorators
前言
之前了解过一些,但是因为没有做专门的笔记,最近学习nest.js的时候,到处都是装饰器,不得已重温了一下ts的装饰器,并弄个笔记记录一下。
typescript提供了几种装饰器:
- 类装饰器
- 方法装饰器
- 属性装饰器
- 访问器装饰器
- 参数装饰器
在使用装饰器之前,我们还需要配置一下tsconfig.json,开启两个配置:
{
"compilerOptions": {
"target": "ESNext",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
}
}
其中target不能低于es5版本,因为装饰器大量用到了描述符对象,而这个对象是在es5版本提供的,不过现在es5标配了。
emitDecoratorMetadata
表示开启元数据支持,同时还需要安装一个依赖:
pnpm i reflect-metadata
安装完成后在你的代码入口处或者需要的地方import引入:
import "reflect-metadata";
这个的东西用于给参数访问器使用的,因为参数访问器是无法拿到具体的参数的,它只有参数的位置,那么为了能够去改变参数或者一些其他操作,往往都是先将参数的下标存到数组中,这个数组存在一个元数据对象中,这个元数据对象可以挂载在当前类或者其他地方。
然后配合类装饰器或者方法装饰器使用,在这些装饰器中我们才可以控制到传入的参数。
experimentalDecorators
表示允许使用装饰器。
装饰器
类装饰器
这个非常好理解,我们的class其实就是构造函数的语法糖,那么类装饰器就是函数套函数的一种方式。
/** 类装饰器 */
const classDecorator: ClassDecorator = (target) => {
target.prototype.name = "我是通过类装饰器添加的";
};
@classDecorator
class Test {
constructor() {}
}
const test = new Test();
console.log(test.name); //我是通过类装饰器添加的
target就是Test
,它作为了参数传给了函数classDecorator
,class是语法糖,Test就是构造函数,所以我们还是可以通过原型的方式进行扩展操作。
但是这里会有一个问题,就是类型的推断:
可以看到新增的属性是无法被准确推断出来的,哪怕我们通过类型重载的方式:
/** 类装饰器 */
const classDecorator = <T extends { new (...args: any[]): {} }>(target: T) => {
// target.prototype.name = "我是通过类装饰器添加的";
return class extends target {
public name = "我是通过类装饰器添加的";
};
};
@classDecorator
class Test {
constructor() {}
}
const test = new Test();
console.log(test.name);
由此可见,其实类装饰器不太适合扩展,它更适合调整默认值,属性或者方法的替换,但是需要保证返回值的一致,因为类型推断出来的还是原来class的结果。
在nest.js里面,类装饰被用来做初始化参数传递了,它有一个@Injectable()
类装饰器,还有@Inject
参数装饰器,参数装饰器通过元数据将需要的参数记录下来,然后在通过类装饰器在运行时在共享的对象中获取Inject指定的对象。
然后将参数传入并实例化。
方法装饰器
方法装饰器会在方法声明后运行,具体看个例子:
/** 方法装饰器 */
const methodDecorator: MethodDecorator = (target, propertyKey, descriptor) => {
console.log(target, propertyKey, descriptor);
};
class Test {
constructor() {}
@methodDecorator
public show() {
console.log("Hello World");
}
}
const test = new Test();
test.show();
// {constructor: ƒ, show: ƒ} 'show' {writable: true, enumerable: false, configurable: true, value: ƒ}
// Hello World
因为我们如果要改动一个东西,肯定要在运行前就改好。
方法装饰器接收三个参数:
target
如果是静态方法,它就是构造函数本身,反之则是类的原型对象propertyKey
成员的名字,也就是方法名descriptor
描述符对象
我们如果要改动一个方法,可以修改描述对象value属性,将value替换成新的方法已实现新的功能。
/** 方法装饰器 */
const methodDecorator: MethodDecorator = (target, propertyKey, descriptor: PropertyDescriptor) => {
descriptor.value = function () {
console.log("hello decorator");
};
};
class Test {
constructor() {}
@methodDecorator
public show() {
console.log("Hello World");
}
}
const test = new Test();
test.show(); // hello decorator
属性装饰器
和方法装饰器差不多,也是在声明后运行,但是它只有两个参数:
target
如果是静态属性,它就是构造函数本身,反之则是类的原型对象propertyKey
属性的名称
/** 属性装饰器 */
const propertyDecorator: PropertyDecorator = (target, propertyKey) => {
console.log(target, propertyKey);
};
class Test {
@propertyDecorator
public name: string = "test";
constructor() {}
}
const test = new Test();
// {constructor: ƒ} 'name'
我们可能会这么修改:
/** 属性装饰器 */
const propertyDecorator: PropertyDecorator = (target: any, propertyKey) => {
console.log(target, propertyKey);
target[propertyKey] = "decorator";
};
class Test {
@propertyDecorator
public name: string = "test";
constructor() {}
}
const test = new Test();
console.log(test); // test
实际上还是test,我们修改的其实是原型,因为public name: string = "test"
转换为的其实是this.name = "test"
,这是实例的属性,装饰器是没法修改的。
可以看到我们的东西确实是在原型上的。
这也间接说明,属性装饰器的应用场景是很小的,官方定义属性装饰器用来监视类中是否声明了某个名字的属性。
所以了解一下即可。
访问器装饰器
访问器装饰器作用于访问器上,说人话就是get\set
上,但是不允许同一个属性get、set都使用访问器装饰器,你只能选择一个,因为它是通过描述符对象进行修改的,这个对象上可以直接改get、set方法,它内部整合了,不需要再单独声明。
它接收三个参数:
target
静态成员是构造函数,反之则是原型对象propertyKey
属性的名称descriptor
描述符对象
/** 访问器装饰器 */
const accessorDecorator = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
let value = "default";
descriptor.get = () => {
return value;
};
descriptor.set = (newValue: string) => {
value = newValue;
};
};
class Test {
private _name: string = "test";
@accessorDecorator
get name() {
return this._name;
}
set name(value: string) {
this._name = value;
}
constructor() {}
}
const test = new Test();
console.log(test.name);
test.name = "new name";
console.log(test.name);
这个比属性访问器稍微强一些,但是还是要注意,实例化后的对象属性,如果不是原型上的我们是改不了的。
参数装饰器
参数装饰器有三个参数:
target
静态成员是构造函数,反之则是原型对象propertyKey
属性的名称parameterIndex
参数的下标
/** 参数装饰器 */
const parameterDecorator: ParameterDecorator = (target, propertyKey, parameterIndex) => {
console.log(target, propertyKey, parameterIndex);
};
class Test {
constructor(@parameterDecorator private name: string) {}
}
const test = new Test("test");
// class { constructor(name) { this.name = name; } } undefined 0
可以看到,有效的就是第一个和第三个参数,而在nest.js中,装饰器会接收一个参数,这个参数可以是string,也可以是具体的类,这个参数会被存起来,作为元数据存在某处,然后通过Injectable
类装饰器将参数进行注入,所以,它的参数如果要传只能是通过@Inject
装饰器进行注入,否则就会报错。
因为类装饰器只能拿到被装饰的类,而不能获取到类实例化时的...args
参数数组,所以,为了防止漏参数,只能报错,必须根据预设规则传参。
具体的做法我贴一个ts的官方例子吧:
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata( requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
let method = descriptor.value!;
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
};
}
装饰器接参
装饰器都是可以接参数的,无非就是多套一层函数就是了。
这里举个参数装饰器的例子:
/** 参数装饰器 */
const parameterDecorator = (type: Number): ParameterDecorator => {
return (target, propertyKey, parameterIndex) => {
console.log(type, target, propertyKey, parameterIndex);
};
};
class Test {
constructor(@parameterDecorator(1) private name: string) {}
}
const test = new Test("test");
// 1 class { constructor(name) { this.name = name; } } undefined 0
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据