当前位置: 首页>编程日记>正文

快速了解 TS 中装饰器的使用

TS 中的装饰器(Decorator)用以对类进行装饰加工。它可以被附加到类的声明,以及类中的成员属性、方法、访问器、方法参数、构造器参数等目标上。

TS 对装饰器提供了官方支持,它作为一个实验性功能,可以在 tsconfig.json 中将 compilerOptions.experimentalDecorators 设置为 true 开启。

装饰器的本质是函数,比如一个装饰器名为 decorator,则以 @decorator 的形式使用。装饰器对类的改变是在代码编译时发生的,编译后的代码中,定义被装饰的类时,装饰器函数会被调用,被装饰的内容相关的数据会作为参数传入装饰器函数中。

装饰器基本使用

以作用于类本身的装饰器为例:

  • 接收一个参数 target,表示被装饰的类本身。
  • 可以返回一个新的类以覆盖被装饰的类,如果不显式返回,则默认返回被装饰的类本身。

示例:

function substitute(target: any) {
  console.log(target); // 输出被装饰的类本身
  return class Substitute {
    sayHello() {
      console.log("Hello Decorator");
    }
  };
}

@substitute
class Person {
  sayHello() {
    console.log("Hello Class");
  }
}

const person = new Person();
person.sayHello(); // 输出:"Hello Decorator"

装饰器工厂

装饰器支持以装饰器工厂的形式使用。装饰器工厂本质是一个用以返回一个装饰器的工厂函数。比如一个装饰器工厂名为 decoratorFactory,则以 @decoratorFactory(args) 的形式使用,这样会将调用 decoratorFactory(args) 返回的装饰器应用到目标上。装饰器工厂使得可以通过传入不同的参数而快速生成不同的装饰器。

同一目标使用多个装饰器

此处使用一个工厂函数 marker 传入一个数字作为参数生成装饰器,装饰器会将该数字赋值给被装饰的类的静态属性 index。分别将该 marker 生成的不同装饰器以及另一个装饰器 breaker 添加给 Person 类:

function marker(n: number) {
  console.log("装饰器工厂 marker 被调用,参数是 " + n);
  return function (target: any) {
    console.log("装饰器函数被调用,其标记的数字为 " + n);
    target.index = n;
  };
}

function breaker() {
  console.log("我是来破坏队形的");
}

@marker(1)
@marker(2)
@breaker
@marker(3)
class Counter {
  static index: number;
}

console.log("被装饰的类最后得到的静态属性 index 的值为 " + Counter.index);

/* 最终控制台输出如下:
    装饰器工厂 marker 被调用,参数是 1
    装饰器工厂 marker 被调用,参数是 2
    装饰器工厂 marker 被调用,参数是 3
    装饰器函数被调用,其标记的数字为 3
    我是来破坏队形的
    装饰器函数被调用,其标记的数字为 2
    装饰器函数被调用,其标记的数字为 1
    被装饰的类最后得到的静态属性 index 的值为 1
*/

可以看出,当对同一个目标使用多个装饰器时,如果使用的是装饰器工厂,则它们会按代码中的顺序从上到下执行得到装饰器,而装饰器本身会按从下到上的顺序执行。

作用于不同目标的装饰器

除了作用于类本身,也可以对类中的其它目标使用装饰器,对于不同的目标,有时装饰器接收的函数有所差别。

作用于方法

装饰器作用于类中的方法时,可以接收三个参数:

  • target:作用于实例方法时是类的 [[prototype]],作用于静态方法时是类本身;
  • name:方法名;
  • descriptor:属性描述符,见 Object.defineProperty() - JavaScript | MDN (mozilla.org)。

如果有返回值,则会作为目标方法的属性描述符。

示例:

function readonly(target: any, name: string, descriptor: PropertyDescriptor) {
  console.log(target === Person.prototype); // 此例中输出:true
  console.log(name); // 此例中输出:"sayHello"
  console.log(descriptor);
  /* 此例中输出:
    {
      value: [Function: sayHello],
      writable: true,
      enumerable: false,
      configurable: true
    }
  */
  descriptor.writable = false;
}

class Person {
  @readonly
  sayHello() {
    console.log("Hello Decorator");
  }
}

console.log(Object.getOwnPropertyDescriptor(Person.prototype, "sayHello"));
/* 输出:
  {
    value: [Function: sayHello],
    writable: false,
    enumerable: false,
    configurable: true
  }
*/

const person = new Person();

// 以下两行代码执行任何一行都会报错,无法修改读取 person.sayHello 得到的值
Person.prototype.sayHello = function () {};
person.sayHello = function () {};

作用于访问器方法

装饰器作用于访问器方法时,接收参数与返回值作用和作用于方法时相同。其中,作用于实例访问器方法时,target 接收的是类的 [[prototype]],作用于静态访问器方法时接收的是类本身。

示例:

function limitAge(limit: number) {
  return function (
    target: any,
    name: string,
    descriptor: TypedPropertyDescriptor<number>
  ) {
    console.log(target === Person.prototype); // 此例中输出:true
    console.log(name); // 此例中输出:"age"
    console.log(descriptor);
    /* 此例中输出:
      {
        get: [Function: get age],
        set: [Function: set age],
        enumerable: false,
        configurable: true
      }
    */
    const originalSetter = descriptor.set;
    descriptor.set = function (age: number) {
      if (age >= limit || age < 0) return;
      if (originalSetter) originalSetter.call(this, age);
    };
  };
}

class Person {
  constructor(private _age: number) {}

  get age() {
    return this._age;
  }

  @limitAge(150)
  set age(age: number) {
    this._age = age;
  }
}

const person = new Person(18);

person.age = 180;
console.log(person.age); // 输出:18

person.age = 81;
console.log(person.age); // 输出:81

作用于成员属性

装饰器作用于成员属性时,接收两个参数:

  • target:作用于实例成员时是类的 [[prototype]],作用于静态成员时是类本身;
  • name:属性名。

返回值会被忽略。

示例:

function checkProperty(target: any, name: string) {
  let targetStr = "";
  if (target === Person.prototype) {
    targetStr = "类的 [[prototype]]";
  } else if (target === Person) {
    targetStr = "类本身";
  } else return
  console.log(`当前属性名为 ${name}, 装饰器接收的第一个参数是${targetStr}`);
}

class Person {
  @checkProperty
  static staticProperty = "vallue";
  @checkProperty
  instanceProperty: string = "value";
}

/* 最终控制台输出如下:
    当前属性名为 instanceProperty, 装饰器接收的第一个参数是类的 [[prototype]]
    当前属性名为 staticProperty, 装饰器接收的第一个参数是类本身
*/

作用于参数

装饰器作用于参数(可以是实例方法、静态方法或构造器中的参数,当然也包括访问器方法)时,接收三个参数:

  • target:作用于实例成员时是类的 [[prototype]],作用于静态成员时是类本身;
  • name:方法名,如果作用于 constructor 中的参数,则接收 undefined;
  • index:目标参数在方法的参数列表中的索引。

返回值会被忽略。

示例:

function checkArg(target: any, name: string | undefined, index: number) {
  let targetStr = "";
  if (target === Test.prototype) {
    targetStr = "类的 [[prototype]]";
  } else if (target === Test) {
    targetStr = "类本身";
  } else return;

  const funName = name ?? "constructor";

  console.log(
    `当前检测的是方法 ${funName} 中的第 ${
      index + 1
    } 个参数,装饰器接收的第一个参数是${targetStr}`
  );
}

class Test {
  static staticFun(arg1: any, @checkArg arg2: any) {}
  constructor(@checkArg arg: any) {}
  instanceFun(@checkArg arg1: any, arg2: any) {}
}

/* 最终控制台输出如下:
    当前检测的是方法 instanceFun 中的第 1 个参数,装饰器接收的第一个参数是类的 [[prototype]]
    当前检测的是方法 staticFun 中的第 2 个参数,装饰器接收的第一个参数是类本身
    当前检测的是方法 constructor 中的第 1 个参数,装饰器接收的第一个参数是类本身
*/

同时也能看出,对实例方法、静态方法、构造器方法的参数分别使用装饰器时,装饰器执行的顺序是:实例 —> 静态 —> 构造器


相关文章: