TypeScript学习记录-装饰器

87 阅读6分钟

装饰器

装饰器提供了一种为类声明和成员添加注释和元编程语法的方法

启用装饰器支持:

命令行:tsc --target ES5 --experimentalDecorators

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

应用场景:

  • 日志记录:在方法或类上添加日志功能,用于记录方法的执行过程和结果
  • 性能监控:在方法或类上添加性能监控功能,用于计算方法的执行时间
  • 权限验证:在方法或类上添加权限验证功能,用于检查用户是否有权执行某个操作
  • 数据验证:在方法或类上添加数据验证功能,用于检查输入数据是否合法
  • 缓存处理:在方法或类上添加缓存处理功能,用于缓存方法的结果

概念

装饰器是一种特殊的声明,可以附加到 类声明、方法、accessor、属性 或 参数。装饰器使用 @expression 形式,其中 expression 必须评估为一个函数,该函数将在运行时调用,并带有有关装饰声明的信息

function sealed(target) {
  // do something with 'target' ...
}

装饰器工厂

自定义如何将装饰器应用于声明,可以编写一个装饰器工厂。装饰器工厂只是一个函数,它返回将由装饰器在运行时调用的表达式

function color(value: string) {
  // this is the decorator factory, it sets up
  // the returned decorator function
  return function (target) {
    // this is the decorator
    // do something with 'target' and 'value'...
  };
}

function decoration(value:number){
  // value 是通过装饰器传进来的值

  // return 的内容将会替换掉原来的构造方法
  return function(target: any){
    // target 获取当前的类
    // 可以在这对类的原型对象进行操作
    console.log(target) // 这里获取到的是 Cat 这个类
    target.prototype.age = value;
  }
}

@decoration(18)
class Cat {
  name: string;

  constructor(name: string){
    this.name = name;
  }
}

let tom: any = new Cat('tom');
console.log(tom.name);  // 'tom'
console.log(tom.age); // 18

多个装饰器执行顺序

可以添加多个装饰器,在 TypeScript 中对单个声明评估多个装饰器时执行以下步骤:

  1. 每个装饰器的表达式都是从上到下计算的
  2. 然后将结果作为函数从下到上调用
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

类装饰器

  • 类声明之前开始
  • 应用于构造函数
  • 用于观察、修改或替换类定义
  • 构造函数作为其唯一参数
  • 如果类装饰器返回一个值,它将用提供的构造函数替换类声明
  • 注意:如果返回新的构造函数,则必须注意维护原始原型。在运行时应用装饰器的逻辑不会执行此操作
function decoration(constructor: Function){
  // 参数是构造函数(也可以说是 Cat 这个类)
  console.log('===》装饰器执行时机《===');
  console.log('===》参数内容《===');
  console.log(constructor);
  console.log('=================');
  console.log('===》为类添加属性《===');
  constructor.prototype.eat = () => {console.log('吃鱼');};
  constructor.prototype.type = '我是猫';
}

@decoration
class Cat {
  name: string;

  constructor(name: string){
    console.log('===》构造器执行时机《===');
    this.name = name;
  }
}

let tom: any = new Cat('tom');
console.log(tom.name); // "tom"

// 注意:由于装饰器无法改变TypeScript的类型,所以type和eat()对于TypeScript来说仍然是未知的
console.log(tom.type); // "我是猫"
tom.eat(); // "吃鱼"

function decoration(value:number){
  // value 是通过装饰器传进来的值

  // return 的内容将会替换掉原来的构造方法
  return function(target: any){
    // target 获取当前的类
    // 可以在这对类的原型对象进行操作
    console.log(target) // 这里获取到的是 Cat 这个类
    target.prototype.age = value;
  }
}

@decoration(18)
class Cat {
  name: string;

  constructor(name: string){
    this.name = name;
  }
}

let tom: any = new Cat('tom');
console.log(tom.name);  // 'tom'
console.log(tom.age); // 18

方法装饰器

  • 在方法声明之前声明
  • 应用于方法的属性描述符,可用于观察、修改或替换方法定义

有以下三个参数:

  1. 静态成员的类的构造函数,或者实例成员的类的原型
  2. 成员的名称
  3. 成员的属性描述符
function enumerable(value: boolean) {
  console.log('===》装饰器执行时机《===');
  return function (target: any,propertyKey: string,descriptor: PropertyDescriptor) {
    console.log('成员构造方法(如果是实例的话就是实例原型)(当前类)===>',target);
    console.log('成员名称(当前方法名称)===>',propertyKey);
    console.log('成员属性描述符===>',descriptor); // 包含 writable(可写),enumerable(可列举),configurable(可配置)
    descriptor.enumerable = value;
  };
}

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }

  @enumerable(false)
  greet() {
    return "Hello, " + this.greeting;
  }
}

const greeter = new Greeter("greeter");
const res = greeter.greet();
console.log(res)

访问器装饰器(getter方法)

参数:

  1. 静态成员的类的构造函数,或者实例成员的类的原型
  2. 成员的名称
  3. 成员的属性描述符
function configurable(value: boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.configurable = value;
  };
}
// ---cut---
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;
  }
}

const point = new Point(1,2);
// 此时不能再修改point的x与y
point.x = 1; // 报错
point.y = 2; // 报错

属性装饰器

参数:

  1. 静态成员的类的构造函数,或者实例成员的类的原型
  2. 成员的名称
import "reflect-metadata";
const formatMetadataKey = Symbol("format");

// 这个注解在执行的时候,会把接收到的字符串模版放到元数据上
// 这样当实例化的时候实例也可以通过元数据重新拿到这个 formatMetadataKey 字符串模版
function format(formatString: string) {
  return Reflect.metadata(formatMetadataKey, formatString);
}

// 从当前对象的元数据中获取 formatMetadataKey 字符串模版
function getFormat(target: any, propertyKey: string) {
  return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

class Greeter {
  @format("Hello, %s")
  greeting: string;
  
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    // 这里是从 greeting 上取字符串模版(装饰器初始化的时候将模版设置进了这个属性的元数据上)
    let formatString = getFormat(this, "greeting");
    return formatString.replace("%s", this.greeting);
  }
}

const greeter = new Greeter('xiaoming');
console.log(greeter.greet()) // "Hello, xiaoming" 

参数装饰器

参数:

  1. 静态成员的类的构造函数,或者实例成员的类的原型
  2. 成员的名称
  3. 函数参数列表中参数的序号索引
// target:静态属性指构造函数,实例属性、实例方法值构造函数的原型
// methodName:方法的名称
// paramIndex:参数的索引
function addAge(target: any, methodName: string, paramIndex: number) {
  // 构造方法:{ login: [Function (anonymous)] } 方法名:login 当前参数的索引:1
  console.log(target, methodName, paramIndex); 
  target.age = 18; // 设置默认值为18
}

class Person {
  age: number|undefined;
  login(username: string, @addAge password: string) {
    console.log(this.age, username, password); // 18 admin admin123
  }
}
let p = new Person();
p.login('admin', 'admin123');

元数据

实验性功能

安装:npm i reflect-metadata --save

命令行设置:tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata

tsconfig.json:

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

启用后,只要导入了 reflect-metadata 库,就会在运行时公开额外的设计时类型信息

// @emitDecoratorMetadata
// @experimentalDecorators
// @strictPropertyInitialization: false
import "reflect-metadata";

class Point {
  constructor(public x: number, public y: number) {}
}

class Line {
  private _start: Point;
  private _end: Point;

  @validate
  set start(value: Point) {
    this._start = value;
  }

  get start() {
    return this._start;
  }

  @validate
  set end(value: Point) {
    this._end = value;
  }

  get end() {
    return this._end;
  }
}

function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
  let set = descriptor.set!;
  
  descriptor.set = function (value: T) {
    let type = Reflect.getMetadata("design:type", target, propertyKey);

    if (!(value instanceof type)) {
      throw new TypeError(`Invalid type, got ${typeof value} not ${type.name}.`);
    }

    set.call(this, value);
  };
}

const line = new Line()
line.start = new Point(0, 0)

// @ts-ignore
// line.end = {}

// Fails at runtime with:
// > Invalid type, got object not Point