什么是装饰器?
装饰器本质上是一个函数,用来扩展类和类的成员
启用装饰器语法
为了能够使用装饰器语法,需要在 tsconfig.json 中将 experimentalDecorators 开启,以此来消除警告。
"experimentalDecorators": true
装饰器用法
因为装饰器本身是一个函数,并且它可以用来修饰类
那么,它的参数意味着什么?
function Decorator(target: any) {
console.log(target);
}
@Decorator
class Animal {}
这个 target 是什么?
实际上,这个 target 就是 class Animal,它指向类本身
另外,target 除了指向类之外,就是类的原型了。
将 ts 文件编译成 js 文件后
var __decorate =
(this && this.__decorate) ||
function (decorators, target, key, desc) {
var c = arguments.length,
r =
c < 3
? target
: desc === null
? (desc = Object.getOwnPropertyDescriptor(target, key))
: desc,
d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i]))
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function Decorator(target) {
console.log(target);
}
var Animal = /** @class */ (function () {
function Animal() {}
Animal = __decorate([Decorator], Animal);
return Animal;
})();
可以看到在__decorate 方法中第二个参数就为 target,而其对应的值就是 Animal。
那么,还有一个疑问:为什么__decorate 方法可以传一个 Decorator 数组?
因为,一个类可以有多个装饰器,并且在最后通过 d 函数去处理装饰器,将结果赋值给 r,最后返回 r。
再观察循环的方式,可以发现:装饰器是倒序执行的,也就是说先执行离自己最近的那个装饰器,再向上执行。实际上,整个执行的流程有点类似于洋葱模型。
扩展类的属性和原型
function Decorator(target: any) {
target.type = "animal";
target.getType = function () {
return this.type;
};
target.prototype.eat = function () {
console.log("eat");
};
target.prototype.run = function () {
console.log("run");
};
}
@Decorator
class Animal {}
console.log((Animal as any).getType());
返回子类,这个子类用于重写父类
function OverwriteAnimal(target: any) {
return class extends target {
eat() {
super.eat(); // parent eat
console.log("child eat");
}
};
}
@OverwriteAnimal
class Animal {
eat() {
console.log("parent eat");
}
}
let animal = new Animal();
console.log(animal.eat()); // child eat
扩展类中的方法
(装饰器函数在修饰成员函数时一定会执行,无论有没有创建实例)
function Enum(isEum: boolean) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
// 此时target指向的是类的原型,key为方法,descriptor为属性描述器
console.log(target, key, descriptor);
descriptor.enumerable = isEum;
// 进行函数增强
let originalValue = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log("eat prev");
originalValue.call(this, ...args);
console.log("eat next");
};
};
}
class Animal {
@Enum(true)
eat() {
console.log("parent eat");
}
}
let animal = new Animal();
console.log(animal.eat());
扩展属性访问器
function valueToUpper(
target: any,
key: string,
descriptor: PropertyDescriptor
) {
let originalValue = descriptor.set;
descriptor.set = function (newValue) {
originalValue?.call(this, newValue.toUpperCase());
};
}
class Animal {
private _value: string = "";
@valueToUpper
set value(newValue: string) {
this._value = newValue;
}
}
let animal = new Animal();
animal.value = "hhhhh";
console.log(animal);
通过这些例子,可以感知出来:
- 如果需要在装饰器中传参的话,需要在装饰器函数外先包一层,但最终还是需要返回一个函数。
装饰器的执行流程
function Decorator(val: string) {
return function (
target: any,
key?: string,
descriptor?: PropertyDescriptor | number
) {
console.log(val);
};
}
@Decorator("类装饰器")
class Flow {
constructor(@Decorator("构造函数参数装饰器") val: string) {}
@Decorator("静态属性装饰器")
static type = "flow";
@Decorator("静态方法装饰器")
static getType() {
return this.type;
}
@Decorator("实例属性装饰器")
public name!: string;
@Decorator("实例/原型方法装饰器")
run(@Decorator("函数参数装饰器") val: string) {}
@Decorator("属性访问器装饰器")
get value() {
return "value";
}
}
一直会是这个固定的顺序吗?
不是。
倘若将静态方法装饰器和静态属性装饰器交换位置,会出现如下图:
由此可得出:方法和属性之间没有顺序关系,谁先写谁先执行。
那么,根据此结论,可以将代码这样分:
function Decorator(val: string) {
return function (
target: any,
key?: string,
descriptor?: PropertyDescriptor | number
) {
console.log(val);
};
}
// 最后执行
@Decorator("类装饰器")
class Flow {
// 倒数第二个执行
constructor(@Decorator("构造函数参数装饰器") val: string) {}
// 以下两个,谁先写谁先执行
@Decorator("静态方法装饰器")
static getType() {
return this.type;
}
@Decorator("静态属性装饰器")
static type = "flow";
// 以下三个,谁先写谁先执行
@Decorator("属性访问器装饰器")
get value() {
return "value";
}
@Decorator("实例/原型方法装饰器")
run(@Decorator("函数参数装饰器") val: string) {}
@Decorator("实例属性装饰器")
public name!: string;
}
这里可能会有一个疑问:明明实例/原型方法装饰器写在了函数参数装饰器之上,怎么和之前说的不一样?
其实很好理解,执行函数时肯定要先找参数,随后在执行函数体中的逻辑,来完成函数的功能。所以会先执行函数参数装饰器,再执行实例/原型方法装饰器。
但整个装饰器执行的流程不变,总体分为:实例 -> 静态 -> 构造函数 -> 类。