4

Typescript 装饰器及应用场景浅析

 2 years ago
source link: https://jelly.jd.com/article/6163d8bac3f2f4019154ee94
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Typescript 装饰器及应用场景浅析
上传日期:2021.11.10
本文旨在对不同种类的装饰器进行学习, 了解装饰器及装饰器工厂的差别,举例应用场景,帮助开发者更好地应用到业务场景中。

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

一、装饰器种类

  • 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()

控制台输出如下:

11d36b920f7b79f2.png
  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.

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK