Typescript 装饰器

92 阅读2分钟

介绍

装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。

装饰器是一种特殊类型的声明,它能够被附加到类声明方法, 访问符属性参数上。

装饰器是一项实验性特性,在未来的版本中可能会发生改变。

JavaScript 中装饰器还在提案阶段,Typescript 中已经支持。

Typescript 中装饰器写法分为两种:

  • stage 2
  • stage 3

主要使用 stage2 写法,因为 stage3 刚出来,很多库没有支持。

怎么用

stage2

要使用装饰器 stage2 写法,需要在tsconfig.json文件中打开两个配置。

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
  }
}

在 stage2 中,装饰器有以下几种

  • 类装饰器
  • 属性装饰器
  • 方法装饰器
  • 参数装饰器
  • 访问装饰器

执行顺序为

  1. 属性装饰器、方法装饰器、访问装饰器按照在类中出现的顺序,从上往下,依次执行
  2. 类装饰器最后执行
  3. 参数装饰器先于方法装饰器执行

类装饰器

/**
 * 类装饰器
 * @param constructor 类本身
 */
function logClass(constructor: Function) {
  console.log(`Class Created ${constructor.name}`, constructor);
}
@logClass
class Person {
  constructor(public name: string) {}
}

// 类装饰器工厂 是一个返回装饰器的函数,可以接收参数来控制装饰器的行为
function logClassWithParams(message: string) {
  return function (constructor: Function) {
    console.log(constructor.name, message);
  };
}
@logClassWithParams("Class Created")
class Car {}

可以使用类装饰器扩展类的功能,比如添加新的属性和方法。

// 类装饰器扩展类的功能,比如说可以添加新的属性和方法
function addTimeStamp<T extends { new (...args: any[]) }>(constructor: T) {
  // 返回一个添加了新属性的类
  return class extends constructor {
    timestamp = new Date();
  };
}

interface Document {
  timestamp: Date;
}
@addTimeStamp
class Document {
  age = 18;
  constructor(public title: string) {}
}
const doc = new Document("My Document");
console.log(doc.title);
console.log(doc.timestamp);
console.log(doc.age);

export {};

可以通过返回一个新的构造函数来替换原有的构造函数。

function addTimeStamp<T extends { new (...args: any[]) }>(
  constructor: T
) {
  console.log(constructor.name);

  return class extends constructor {
    constructor(...args) {
      super(...args);
      console.log("instance");
    }
  };
}

@addTimeStamp
class User {
  constructor(public name: string) {
    console.log("user");
  }
}
const doc = new User("leo");
console.log(doc.name);

export {};

方法装饰器

方法装饰器可以在方法调用时做日志记录,也可以做权限检查、缓存方法执行结果等。

/**
 * 方法装饰器
 * @param target 装饰的目标对象,如果是静态成员,则是类的构造函数,如果是实例成员,则是类的原型对象
 * @param propertyKey 装饰的成员名称
 * @param descriptor 成员的属性描述符
 */
function log(target, propertyKey, descriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with arguments:${args}`);
    const result = originalMethod.apply(this, args);
    console.log(`Result:${result}`);
    return result;
  };
}

class Calculator {
  @log
  add(a: number, b: number) {
    return a + b;
  }
}
const calutor = new Calculator();
calutor.add(1, 2);

访问器装饰器

访问器装饰器是用来装饰访问器的 get set。

function configurable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      descriptor.configurable = value;
  };
}
class Point {
  private _x: number;
  private _y: number;
  constructor(x: number, y: number) {
      this._x = x;
      this._y = y;
  }

  @configurable(false)
  get x() { return this._x; }

  @configurable(false)
  get y() { return this._y; }
}

属性装饰器

// 属性访问控制
// 用属性装饰器来进行访问控制或者设置初始设置

function defaultValue(value: any) {
  console.log(`defaultValue`, value);

  return function (target: any, propertyKey: string) {
    let val = value;
    const getter = function () {
      return val;
    };
    const setter = function (newValue) {
      console.log("newValue", newValue);
      val = newValue;
    };
    Object.defineProperty(target, propertyKey, {
      enumerable: true,
      configurable: true,
      get: getter,
      set: setter,
    });
  };
}

class Setting {
  @defaultValue("dark")
  theme: string;
  @defaultValue(30)
  timeout: number;
}
const setting = new Setting();
console.log(setting.theme);
console.log(setting.timeout);

参数装饰器

用于装饰类的构造函数或方法的参数。

const REQUIRED_PARAMETERS = "REQUIRED_PARAMETERS";
/**
 *
 * @param target 装饰的目标对象
 * @param propertykey 参数所属的方法名称
 * @param parameterIndex 参数在参数列表中的索引
 */
function validate(
  target: any,
  propertykey: string,
  parameterIndex: number
) {
  const existingRequiredParameters: number[] =
    Reflect.getOwnMetadata(
      REQUIRED_PARAMETERS,
      target,
      propertykey
    ) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata(
    REQUIRED_PARAMETERS,
    existingRequiredParameters,
    target,
    propertykey
  );
}

function validateParameters(
  target: any,
  propertykey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    const existingRequiredParameters: number[] =
      Reflect.getOwnMetadata(
        REQUIRED_PARAMETERS,
        target,
        propertykey
      ) || [];
    for (let parameterIndex of existingRequiredParameters) {
      if (args[parameterIndex] === undefined) {
        throw new Error(
          `Missing required arguments at position ${parameterIndex}`
        );
      }
    }

    return originalMethod.apply(this, args);
  };
}

class User {
  constructor(private name: string, age: number) {}
  @validateParameters
  setName(newName: string, @validate age: number) {
    this.name = newName;
  }
}
const user = new User("aa", 22);
user.setName("Bob", undefined);