装饰器

可以对类、类的属性(数据属性、访问器属性)和方法、方法的参数进行包装,扩展或修改原来的功能。

目前作为一项实验性的特性,需要在tsconfig.json配置中打开Experimental Options选项
/* Experimental Options */
    "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
    "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
装饰器是一种特殊类型的声明,该声明可以被附加在类声明、方法声明、访问器属性、数据属性或参数上
声明形式

使用@expression形式来使用,其中expression求值后必须是函数。expression可以直接是函数,也可以是函数调用(该方式称为工厂模式)
这里以类修饰器为例,如:
expression为函数时

// 装饰器,传入类的构造函数
function testDecorator(constructor: any) {
  // 下面对类进行修饰
  constructor.prototype.getName = () => {
    console.log('增加了getName方法');
  };
  console.log('decorator');
}
// 对类Test进行装饰,此时的expression即为装饰器函数
@testDecorator 
class Test {}

expression为函数调用(工厂模式时)

// 通过工厂函数的参数flag可以决定是否调用装饰器
function testFactoryDecorator(flag) {
	if (flag) {
    return function(constructor: any) {
      constructor.prototype.getValue = () => {
        console.log('getValue');
      };
    };
  } else {
    return function(constructor: any) {};
  }
}
@testFactoryDecorator(true)
class Test {}
修饰方式

装饰器应用到某个声明x上,可以采用两种形式
写在同一行:@f @g x 写在不同行:

@f
@g
x

当多个装饰器如上文的@f, @g同时应用到某个声明x上时,按复合函数的求值顺序执行,即进行如下步骤的操作:

  1. 由上至下依次对装饰器表达式求值
  2. 求值的结果会被当作函数,由下至上依次调用

这也有点像调用栈一样,求值的过程就像函数入栈,求值结果的函数从调用栈中依次调用。

类中不同声明上的装饰器的应用顺序:
  • 按类的成员分,按实例成员 -> 静态成员 -> 构造函数 -> 类的顺序
  • 按装饰器修饰的类型分,按参数装饰器 -> 方法、访问符、属性

即:

  • 参数装饰器,其次是方法,访问符,或属性装饰器应用到每个实例成员。
  • 参数装饰器,其次是方法,访问符,或属性装饰器应用到每个静态成员。
  • 参数装饰器应用到构造函数。
  • 类装饰器应用到类。
类装饰器
声明方式:类装饰器在类声明之前被声明(紧靠着类声明)。
  • 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中(.d.ts),也不能用在任何外部上下文中(比如declare的类)。
  • 类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数
  • 如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明
  • 注意如果在装饰器里要返回一个新的构造函数,必须注意处理好原来的原型链。 在运行时的装饰器调用逻辑中不会做这些。
类装饰器的标准写法
不同的类的constructor是不同的,所以这里用泛型来写类型
new () => {} 这里表示是构造函数,而T继承构造函数
// 重载构造函数的装饰器例子
function testStandardDecorator<T extends new (...args: any[]) => any>(
  constructor: T
) {
  // 通过返回一个继承自constructor的class来进行对constructor的修改,处理好原型链关系
  return class extends constructor {
    // 拓展constructor
    name = 'lee'; // 将name属性值赋值为'lee'
    getName() {return this.name} // 增加getName()方法
  };
}

@testStandardDecorator
class TestStandard {
  name: string;
  constructor(name: string) {
    this.name = name;
    console.log(this.name); 
  }
}
const test = new TestStandard('hello'); // 先执行constructor,后执行decorator
console.log(test.name); // 先执行constructor,后执行decorator,所以是hello, lee
test.getName(); // typescript会报错,拓展了构造函数后,不知道为什么实例访问拓展后的方法会报错

为了解决装饰器添加的方法实例访问报错的问题,需要按如下方式改写:

// 装饰器用工厂模式改写
function testStandardDecoratorFactory() {
  return function<T extends new (...args: any[]) => any>(constructor: T) {
    // 通过返回一个继承自constructor的class来进行对constructor的修改
    return class extends constructor {
      // 拓展constructor
      name = 'lee'; // 将name属性值赋值为'lee'
      getName() {
        return this.name;
      } // 将getName()方法写在装饰器上
    };
  };
}
// 调用装饰器工厂函数后返回装饰器函数,再传入一个class作为参数,表明装饰器函数修饰的是该class,最后返回被装饰过的class
const teststandard = testStandardDecoratorFactory()(
  class {
    name: string;
    constructor(name: string) {
      this.name = name;
      console.log(this.name); 
    }
  }
);

const test = new teststandard('hello');
console.log(test.getName()); // 此时typescript就不会再报错了
方法装饰器:方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。
  • 方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。
  • 方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(比如declare的类)中。

方法装饰器会被传入3个参数,分别是target,key,descriptor

  • target:对于实例方法,target指向类的prototype,对于静态方法,target指向类的constructor
  • key:指的是方法名
  • descriptor:指的是方法对应的属性描述符,即该属性的描述特性,如enumerable(可枚举)、configurable(可配置)、writable(可修改属性值)、value(属性值)
// 实例方法getName()方法的装饰器,接收参数为
// target:对应的是类的prototype;
// key: 对应的是被装饰的方法名;
// descriptor: 对应的是该方法(属性)的描述符
function getNameDecorator(
  target: any,
  key: string,
  descriptor: PropertyDescriptor
) {
  // console.log(target, key);
  descriptor.value = function() {
    return 'decorator';
  }; // 对getName()方法进行了变更;
}
// 静态方法getValue()方法的装饰器,target对应的是类的构造函数
function getValueDecorator(target: any, key: string) {}
class Test3 {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  // 对类中的方法进行装饰,在类定义好后立即装饰该方法
  @getNameDecorator
  getName() {
    return this.name;
  }
  // 如果是静态方法
  @getValueDecorator
  static getValue() {
    return '123';
  }
}
访问器属性装饰器:访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)
  • 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。
  • 注意  TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。

访问器装饰器传入的参数与方法装饰器相同。如果访问器装饰器返回一个值,它会被用作方法的属性描述符。

// 访问器属性的装饰器,跟普通实例方法的参数一致
function visitDecorator(
  target: any,
  key: string,
  descriptor: PropertyDescriptor
) {
  descriptor.writable = false; // 不允许重写访问器属性
}
class Test4 {
  private _name: string;
  constructor(name: string) {
    this._name = name;
  }
  // 给访问器属性增加装饰器,访问器属性的getter和setter是一组,只能给其中一个增加同名的装饰器,对这组访问器属性有效
  get name() {
    return this._name;
  } //name的getter
  // 给setter增加装饰器
  @visitDecorator
  set name(name: string) {
    this._name = name;
  }
}
const test4 = new Test4('dell');
// test4.name = '123';
console.log(test4.name);
属性装饰器:属性装饰器声明在一个属性声明之前(紧靠着属性声明)
  • 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。

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

  • target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • key:属性名
// 数据属性的装饰器,没有descriptor参数,对实例的数据属性没法在装饰器中修改值
function nameDecorator(target: any, key: string): any {
  const descriptor: PropertyDescriptor = {
    writable: true,
    value: 'lee'
  };
  return descriptor; // 用return的descriptor替换Test5.name属性的descriptor
}
class Test5 {
  // 给属性添加装饰器,改变属性的descriptor
  @nameDecorator
  name = 'dell';
}
const test5 = new Test5();
console.log(test5.name); // lee
参数装饰器:参数装饰器声明在一个参数声明之前(紧靠着参数声明)
  • 参数装饰器应用于类构造函数或方法声明
  • 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare的类)里。
  • 参数装饰器的返回值会被忽略。
  • 注意  参数装饰器只能用来监视一个方法的参数是否被传入。

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

  • target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • key:方法名
  • index:参数的位置
在这里插入// 参数装饰器,其中target为原型,key为方法名,paramIndex参数表示参数的位置
function paramsDecorator(target: any, key: string, paramIndex: number): any {
  console.log(paramIndex);
}
class Test6 {
  // 对参数进行修饰
  getInfo(@paramsDecorator name: string, age: number) {
    console.log(name, age);
  }
}
const test6 = new Test6();
test6.getInfo('Dell', 30);

元数据

reflect-metadata库来支持实验性的metadata API。TypeScript支持为带有装饰器的声明生成元数据。 你需要在命令行或 tsconfig.json里启用emitDecoratorMetadata编译器选项。
可以给类、类中的方法等利用装饰器添加元数据。

导入reflect-metadata库

安装reflect-metadata包

npm install reflect-metadata --save

在项目文件中导入

import "reflect-metadata"
使用API

可以给对象上定义元数据

const user = {
	name:'xxx'
}
Reflect.defineMetadata('data', 'test', user); // 在user上定义了元数据,键值对为data: 'test',定义在user上。
console.log(Reflect.getMetadata('data', user)); // 通过Reflect.getMetadata方法来获取

一般应用在类上,类中方法上

// 利用库本身提供的Reflect.metadata()装饰器工厂函数,给类User添加装饰器,定义元数据
@Reflect.metadata('data', 'test')
class User {
	name = 'xxx';
}
// 给类Teacher的getName()方法添加装饰器,定义元数据
class Teacher {
	@Reflect.metadata('data', 'test')
	getName() {}
}
console.log(Reflect.getMetadata('data', Teacher.prototype, 'getName')); // 获取到Teacher的getName()方法上定义的元数据
// 自定义一个装饰器工厂函数
function setData(data: string, msg: string) {
  return function(target: User, key: string) {
    Reflect.defineMetadata(data, msg, target, key);
  };
}
class Teacher {
// 给getAge()方法添加装饰器,定义元数据
	@setData('data', 'test')
	getAge() {}
}