介绍

在一些场景下需要额外的特性来支持标注或修改类及成员,为我们在类的声明以及成员上通过元编程语法添加标注提供了一种方式

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

启动:在tsconfig.json中启用experimentalDecorators编译选项

定义:

  • 装饰器是一种特殊类型声明,他能够被附加到类声明,方法,访问符,属性或参数上
  • 装饰器使用@expression形式,expression求值后必须为一个函数,他他会在运行时被调用,装饰器的声明信息作为参数传入
function decoratFn() {
  console.log("decoratFn(): evaluated");
  return function (target, propertyKey: string, descriptor: PropertyDecorator){
    console.log('decoratFn(): called');
  }
}
function decoratG() {
  console.log('decoratG(): evaluated');
  return function (target, propertyKey: string, descriptor: PropertyDecorator){
    console.log('decoratG(): called');
  }
}
class DecoratC {
  @decoratFn()
  @decoratG()
  method() {}
}

let desc = new DecoratC();

装饰器工厂:如果要定制一个装饰器如何应用到一个声明上,需要些一个装饰器工厂:就是一个简单的函数,返回一个表达式,以供装饰器在运行时调用 装饰器组合:多个装饰器可以同时应用到一个声明上

注:多个装饰器应用到一个声明上时,他们求值方式与复合函数相似,如:复合f跟g时,复合结果(f.g)(x)等同于f(g(x))

在TS里,当多个装饰器应用在一个声明上时,会进行如下步骤操作:

  • 由上至下依次对装饰器表达式求值
  • 求值的结果会被当做函数,由上到下依次调用

类中不同声明上的装饰器按一下规定顺序应用:

  • 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员
  • 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员
  • 参数装饰器应用到构造函数
  • 类装饰器应用到类

类装饰器

  • 声明:装饰器在类声明之前被声明(紧靠着类声明)
  • 应用:装饰器应用于构造函数,可以用来监视,修改或替换类定义
  • 注意:类装饰器不能用在声明文件中(.d.ts),也不能用在任何外部上下文中
  • 调用:装饰器类表达式会在运行时当做函数调用,类的构造函数作为其唯一参数
  • 返回值:如果累装饰器返回一个值,他会使用提供的构造函数来替换类的声明
  • 返回值注意:如果要返回一个新的构造函数,必须注意处理好原来的原型链,在运行时的装饰器调用逻辑中不会处理原型链
// 类装饰器,处理原型链问题, 装饰类密封当前类
function sealed(constructor: Function){
  console.log('装饰器:sealed')
  Object.seal(constructor);
  Object.seal(constructor.prototype)
}
// 当@sealed被执行的时候,他将密封此类的构造函数和原型
@sealed
class Greeter {
  greeting: string;
  constructor(message: string){
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}
let greeter = new Greeter('Bob');
greeter.greet();
// 重载构造函数
function classDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    newProperty = 'new property';
    hello = 'override';
  }
}
@classDecorator
class Greeters {
  property = 'property';
  hello: string;
  constructor(m: string){
    this.hello = m;
  }
}
console.log(new Greeters('world'));

方法装饰器

  • 声明:声明在一个方法的声明之前(紧靠着方法声明)
  • 应用:会被应用到方法的属性描述符上,可以用来监视,修改或替换方法定义
  • 注意:方法装饰器不能用在声明文件(.d.ts),重载或者任何外部上下文
  • 调用:方法装饰器表达式会在运行时当做函数被调用
  • 返回值:如果方法装饰器返回一个值,他会被用作方法的属性描述

方法装饰器传入下列三个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 成员的名字
  • 成员的属性描述
  • 注:如果代码输出目标小于ES5,属性描述符将会是undefined
// 方法装饰器
function enumerable(value: boolean) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor){
    descriptor.enumerable = value;
  }
}
class GreeterM {
  greeting: string;
  constructor(message: string){
    this.greeting = message;
  }

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

访问装饰器

  • 声明:访问装饰器声明在一个访问器的声明之前(紧靠着访问器声明)
  • 应用:应用于访问器的属性描述符并且可以用来监视,修改或替换一个访问器的定义
  • 注意:访问装饰器不能用在声明文件中(.d.ts),或者任何外部上下文里
  • 使用注意:TS不允许同时装饰一个成员的get和set访问器,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上,因为:装饰器应用于一个属性描述符时,他联合了get和set访问器而不是分开声明的
  • 调用:访问器装饰器表达式会在运行时当做函数被调用
  • 返回值:如果访问器装饰器返回一个值,他会被用作方法的属性描述符

访问器装饰器传入下列三个参数:

  • 对于静态成员来说是类的构造函数,对于实例陈冠是类的原型对象
  • 成员的名字
  • 成员的属性描述
  • 如果代码输出目标版本小于ES5,Property Descriptor将会是undefined
// 访问器装饰器
function configruable(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;
  }

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

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

属性装饰器

  • 声明:属性装饰器声明在一个属性声明之前(紧靠着属性声明)
  • 注意:属性撞死器不能用在声明文件中(.d.ts),或者任何外部上下文里
  • 调用:属性装饰器会在运行时当做函数被调用

属性装饰器传入两个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 成员的名字
  • 注:属性描述符不会作为参数传入属性装饰器,这与TS如何初始化装饰器有关,因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没有办法监视或修改一个属性的初始化方法,返回值也会被忽略,因此属性描述符只能用来监视类中是否生命了某个名字的属性
// 属性修饰符
import "reflect-metadata";
const formatMetadataKey = Symbol('format');
function format(formatString: string){
  return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
  return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
// @format("Hello, %s")装饰器是个 装饰器工厂。 当 @format("Hello, %s")被调用时,它添加一条这个属性的元数据,
// 通过reflect-metadata库里的Reflect.metadata函数。 当 getFormat被调用时,它读取格式的元数据
class GreeterP {
  @format('Hello, %s')
  greeting: string;

  constructor(message: string){
    this.greeting = message;
  }
  greet() {
    let formatString = getFormat(this, 'greeting');
    return formatString.replace("%s", this.greeting);
  }
}

let greep = new GreeterP('world');
let greepStr = greep.greet();

参数装饰器

  • 声明:参数装饰器声明在一个参数声明之前(紧靠着参数声明)
  • 应用:参数装饰器应用于类构造函数或方法声明
  • 注意:参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文里
  • 调用:参数装饰器表达式会在运行时当作函数被调用
  • 参数装饰器的返回值会被忽略

参数装饰器传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 成员的名字
  • 参数在函数参数列表中的索引
  • 注:参数装饰器只能用来监视一个方法的参数是否被传入
// 参数装饰器
class GreeterParam{
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  @validate
  greeter(@required name: string){
    return "Hello, " + name + ", " + this.greeting;
  }
}

// @required装饰器添加了元数据实体把参数标记为必需的。 @validate装饰器把greet方法包裹在一个函数里在调用原先的函数前验证函数参数
import "reflect-metadata";
const requiredMetadataKey = Symbol('required');
function required(target: Object, propertyKey: string | symbol, parameterIndex: number){
  let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>){
  let method = descriptor.value;
  descriptor.value = function(){
    let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
    if(requiredParameters){
      for(let parameterIndex of requiredParameters){
        if(parameterIndex >= arguments.length || arguments[parameterIndex] === undefined){
          throw new Error("Missing required argument.");
        }
      }
    }
    return method.apply(this, arguments);
  }
}

let greetParam = new GreeterParam('world');
let paramStr = greetParam.greeter('hahaha');
console.log(paramStr)