引言
本文旨在对不同种类的装饰器进行学习, 了解装饰器及装饰器工厂的差别,举例应用场景,并浅析装饰器原理。
一、装饰器种类
- 1、Class Decorators - 类装饰器
类装饰器在类声明之前声明, 类装饰器应用于类的构造函数,可用于观察、修改或替换类定义。
1.1 类装饰器的表达式将在运行时作为函数调用,被装饰类的构造函数将作为它的唯一参数。
function decorateClass<T>(constructor: T) {
console.log(constructor === A) // true
}
@decorateClass
class A {
constructor() {
}
}
上述代码可以看出类装饰器接收的参数constructor === A.prototype.constructor
,即constructor
为class A
的构造函数。
1.2 如果类装饰器返回一个构造函数, 它会使用提供的构造函数来替换类之前的声明。
function decorateClass<T extends { new (...args: any[]): {} }>(constructor: T){
return class B extends constructor{
name = 'B'
}
}
@decorateClass
class A {
name = 'A'
constructor() {
}
}
console.log(new A().name) // 输出 B
- 2、 Method Decorators - 方法装饰器
方法装饰器在方法声明之前声明。装饰器可以应用于方法的属性描述符,并可用于观察、修改或替换方法定义。
2.1 方法装饰器的表达式将在运行时作为函数调用,带有以下三个参数:
- target: 当其装饰静态成员时为类的构造函数,装饰实例成员时为类的原型对象。
- key: 被装饰的方法名。
- descriptor: 成员的属性描述符 即
Object.getOwnPropertyDescriptor(target,key)
。
function decorateMethod(target: any,key: string,descriptor: PropertyDescriptor){
console.log('target === A',target === A) // 是否类的构造函数
console.log('target === A.prototype',target === A.prototype) // 是否类的原型对象
console.log('key',key) // 方法名
console.log('descriptor',descriptor) // 成员的属性描述符 Object.getOwnPropertyDescriptor
}
class A {
@decorateMethod // 输出 true false 'staticMethod' Object.getOwnPropertyDescriptor(A,'sayStatic')
static staticMethod(){
}
@decorateMethod // 输出 false true 'instanceMethod' Object.getOwnPropertyDescriptor(A.prototype,'sayInstance')
instanceMethod(){
}
}
2.2 如果方法装饰器返回一个值,它会被用作方法的属性描述符。
function decorateMethod(target: any,key: string,descriptor: PropertyDescriptor){
return{
value: function(...args: any[]){
var result = descriptor.value.apply(this, args) * 2;
return result;
}
}
}
class A {
sum1(x: number,y: number){
return x + y
}
@decorateMethod
sum2(x: number,y: number){
return x + y
}
}
console.log(new A().sum1(1,2)) // 输出3
console.log(new A().sum2(1,2)) // 输出6
上述代码可以看出sum
被decorateMethod
装饰后,其返回值发生了变化
- 3、Accessor Decorators - 访问器装饰器
访问器装饰器在访问器声明之前声明。访问器装饰器应用于访问器的属性描述符,并可用于观察、修改或替换访问器的定义。
3.1 访问器装饰器与方法装饰器有诸多类似,接受3个参数:
- target: 当其装饰静态成员时为类的构造函数,装饰实例成员时为类的原型对象。
- key: 被装饰的成员名。
- descriptor: 成员的属性描述符 即
Object.getOwnPropertyDescriptor(target,key)
。
function configurable (target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.configurable = false
};
class A {
_age = 18
get age(){
return this._age
}
@configurable
set age(num: number){
this._age = num
}
}
3.2 如果访问器装饰器返回一个值,它会被用作访问器的属性描述符。
function configurable (target: any, key: string, descriptor: PropertyDescriptor) {
return {
writable: false
}
};
class A {
_age = 18
@configurable
get age(){
return this._age
}
set age(num: number){
this._age = num
}
}
const a = new A()
a.age = 20 // 抛出 TypeError: Cannot assign to read only property 'age'
- 4、Property Decorators - 属性装饰器
属性装饰器在属性声明之前声明,返回值会被忽略。
4.1 属性装饰器的表达式将在运行时作为函数调用,带有以下两个参数:
- target: 当其装饰静态成员时为类的构造函数,装饰实例成员时为类的原型对象。
- key: 被装饰的成员名。
function decorateAttr(target: any, key: string) {
console.log(target === A)
console.log(target === A.prototype)
console.log(key)
}
class A {
@decorateAttr // 输出 true false staticAttr
static staticAttr: any
@decorateAttr // 输出 false true instanceAttr
instanceAttr: any
}
- 5、Paramter Decorators - 参数装饰器
参数装饰器在参数声明之前声明,返回值会被忽略。
5.1 参数装饰器的表达式将在运行时作为函数调用,带有以下三个参数:
- target: 当其装饰静态成员时为类的构造函数,装饰实例成员时为类的原型对象。
- key: 参数名。
- index: 参数在函数参数列表中的索引。
function required(target: any, key: string, index: number) {
console.log(target === A)
console.log(target === A.prototype)
console.log(key)
console.log(index)
}
class A {
saveData(@required name: string){} // 输出 false true name 0
}
二、装饰器工厂
不同类型装饰器本身参数是固定的,在运行时被调用,当我们需要自定义装饰器参数时,便可以来构造一个装饰器工厂函数,如下便是一个属性装饰器工厂函数,支持自定义传参name
、age
:
function decorateAttr(name: string, age: number) {
return function (target: any, key: string) {
Reflect.defineMetadata(key, {
name, age
}, target);
}
}
三、执行顺序
ts规范规定装饰器工厂函数从上至下
开始执行,装饰器函数从下至上
开始执行
function first() {
console.log("first(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("first(): called");
};
}
function second() {
console.log("second(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("second(): called");
};
}
class ExampleClass {
@first()
@second()
method() {}
}
控制台输出如下,类似中间件的洋葱模型
:
first(): factory evaluated
second(): factory evaluated
second(): called
first(): called
四、应用场景
- 逻辑层消除繁琐的try/catch块,装饰器内统一输出函数日志
function log (target: any, key: string, value: PropertyDescriptor){
return {
value: async function (...args) {
try{
await value.value.apply(this, args)
}catch(e){
console.log(e)
}
}
};
};
class A {
@log
syncHandle(){
return 3 + a
}
@log
asyncHandle(){
return Promise.reject('Async Error')
}
}
new A().syncHandle()
new A().asyncHandle()
控制台输出如下:
- 校验参数或返回值类型
function validate(){
return function (target: any, name: string, descriptor:PropertyDescriptor) {
let set = descriptor.set
descriptor.set = function (value) {
let type = Reflect.getMetadata("design:type", target, name);
console.log(type.name)
if (!(new Object(value) instanceof type)) {
throw new TypeError(`Invalid type, got ${typeof value} not ${type.name}.`);
}
set?.call(this, value);
}
}
}
class A {
_age: number
constructor(){
this._age = 18
}
get age(){
return this._age
}
@validate()
set age(value: number){
this._age = value
}
}
const a= new A()
a.age = 30
a.age = '30' // 抛出 TypeError: Invalid type, got string not Number.