【前端面试专栏】Typescript 装饰器及应用场景浅析

910 阅读5分钟

引言

本文旨在对不同种类的装饰器进行学习, 了解装饰器及装饰器工厂的差别,举例应用场景,并浅析装饰器原理。

一、装饰器种类

  • 1、Class Decorators - 类装饰器

类装饰器在类声明之前声明, 类装饰器应用于类的构造函数,可用于观察、修改或替换类定义。

1.1 类装饰器的表达式将在运行时作为函数调用,被装饰类的构造函数将作为它的唯一参数。

function decorateClass<T>(constructor: T) {
    console.log(constructor === A) // true
}
@decorateClass
class A {
    constructor() {
    }
}

上述代码可以看出类装饰器接收的参数constructor === A.prototype.constructor,即constructorclass 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

上述代码可以看出sumdecorateMethod装饰后,其返回值发生了变化

  • 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
}

二、装饰器工厂

不同类型装饰器本身参数是固定的,在运行时被调用,当我们需要自定义装饰器参数时,便可以来构造一个装饰器工厂函数,如下便是一个属性装饰器工厂函数,支持自定义传参nameage

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

四、应用场景

  1. 逻辑层消除繁琐的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()

控制台输出如下:

  1. 校验参数或返回值类型
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.

文章篇幅较长,感谢大家的阅读,希望各位看客能够有所收获~ ~ ~

如果这篇文章对你有帮助,欢迎关注我的博客