装饰器
可以对类、类的属性(数据属性、访问器属性)和方法、方法的参数进行包装,扩展或修改原来的功能。
目前作为一项实验性的特性,需要在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上时,按复合函数的求值顺序执行,即进行如下步骤的操作:
- 由上至下依次对装饰器表达式求值
- 求值的结果会被当作函数,由下至上依次调用
这也有点像调用栈一样,求值的过程就像函数入栈,求值结果的函数从调用栈中依次调用。
类中不同声明上的装饰器的应用顺序:
- 按类的成员分,按实例成员 -> 静态成员 -> 构造函数 -> 类的顺序
- 按装饰器修饰的类型分,按参数装饰器 -> 方法、访问符、属性
即:
- 参数装饰器,其次是方法,访问符,或属性装饰器应用到每个实例成员。
- 参数装饰器,其次是方法,访问符,或属性装饰器应用到每个静态成员。
- 参数装饰器应用到构造函数。
- 类装饰器应用到类。
类装饰器
声明方式:类装饰器在类声明之前被声明(紧靠着类声明)。
- 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中(.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() {}
}