# 装饰器
除了采用继承和混入对类进行扩展,装饰器语法提供了一种新的方式。使用装饰器前确保, tsconfig.json 中打开了 experimentalDecorators 配置。
{
//...
"compilerOptions": {
// 打开装饰器配置
"experimentalDecorators": true
// ...
}
}
# 装饰器概述
装饰器是处于 草案阶段 stage2 (opens new window) 的 ECMAScript 语法,Typescript 基于此草案实现了改特性。
装饰器语法实现了对类的动态扩充,避免了直接对类的修改。是实现 面向切面编程 AOP (opens new window) 的一种手段。
TypeScript 提供了一系列装饰器语法来实现对类的动态修改,TypeScript 和 js 草案在装饰器实现上有差异 罗列如下:
| 装饰器 | 功能 | ts | js 草案 |
|---|---|---|---|
| 类装饰器 | 扩充类 | ✅ | ✅ |
| 类方法装饰器 | 修改类的方法 | ✅ | ✅ |
| 类静态方法装饰器 | 修改类的静态方法 | ✅ | ✅ |
| 类属性装饰器 | 修改类的属性 | ✅ | ✅ |
| 类静态属性装饰器 | 修改类的静态属性 | ✅ | ✅ |
| 类访问器属性装饰器 | 修改类的访问器属性 | ✅ | ✅ |
| 类参数装饰器 | 修改类方法的参数 | ✅ | ⬜ |
| 装饰器工厂 | 动态设置装饰器 | ✅ | ✅ |
| 装饰器组合 | 混合多个装饰器 | ✅ | ✅ |
为什么没有函数的装饰器,详细讨论参见 decorators for functions (opens new window), 目前原因为函数提升导致了函数装饰器的不易实现,github 存在如下的 function-decorators-proposal (opens new window) 解决此问题
# 语法详解
# 类装饰器
// TODO: 此处扩展示例需修改
// 1. 定义类装饰器函数,传入为类构造器
function testAble(target: any) {
target._test = true;
}
/**
* 2. 在类之前采用 @<函数名> 生命类装饰器
* 改代码会在代码运行时执行如下逻辑
* class Class1 {};
* testAble(Class1); // 动态增加属性
* */
@testAble
class Class1 {
}
console.log(Class1)也可以返回一个新的类
// TODO: 此处扩展示例需修改
// 1. 定义装饰器返回新的构造器
function classDecorator<T extends { new (...args: any[]): {} }>(
constructor: T
) {
return class extends constructor {
newProperty = "new property";
};
}
@classDecorator
class Class2 {
a = 1;
}
console.log(Class2)
console.log(new Class2())# 方法装饰器
// TODO: 此处扩展示例需修改
// 1. 定义类装饰器函数,传入为类构造器
function dicription(target: any, key: String) {
console.log(`decrption method ${key}`, arguments);
}
class Class3 {
@dicription
/**
* 2. 在方法之前采用 @<函数名> 设置装饰器
* 改代码会在运行时,效果如下
* class Class3 { method1() { // ... } };
* dicription(Class3.prototype,'method1',Object.getOwnPropertyDescriptor('method1') )
* */
method1() {
console.log('method1')
}
}
console.log(Class3)
console.log(new Class3().method1())若想在函数运行时也执行装饰器的注入逻辑,可修改方法描述对象的 value 值
// TODO: 此处扩展示例需修改
// 1. 定义类装饰器函数,传入为类构造器
function dicription1(target: any, key: PropertyKey , val: PropertyDescriptor) {
console.log('run in init', key, val);
let { value: method } = val;
function change(this: Class4) {
console.log('run in method call', this);
console.log(`decrption method ${key as string}`, arguments);
// 注意此处采用 call 防止 this 丢失
method.call(this, ...arguments)
}
// 修改原始函数
val.value = change
console.log('change method success', Object.getOwnPropertyDescriptor(target, key))
}
class Class4 {
/**
* 2. 在方法之前采用 @<函数名> 设置装饰器
* 改代码会在运行时,效果如下
* class Class3 { method1() { // ... } };
* dicription(Class3.prototype,'method1',Object.getOwnPropertyDescriptor('method1') )
* */
@dicription1
method1(...args:any) {
console.log('method1', args, this.method1)
}
}
console.log(Class4)
console.log(new Class4().method1(1,2,3))TIP
注意示例中高亮行,修改方法运行时需避免 this 的丢失
也可直接返回修改的方法描述对象
// TODO: 此处扩展示例需修改
// 1. 定义类装饰器函数,传入为类构造器
function dicription(target: any, key: PropertyKey , val: PropertyDescriptor) {
return {
value: function() {
console.log('run in method call', this);
console.log(`decrption method ${key as string}`, arguments);
// 注意此处采用 call 防止 this 丢失
return val.value.call(this, ...arguments)
}
}
}
class ClassTest {
/**
* 2. 在方法之前采用 @<函数名> 设置装饰器
* 改代码会在运行时,效果如下
* class Class3 { method1() { // ... } };
* dicription(Class3.prototype,'method1',Object.getOwnPropertyDescriptor('method1') )
* */
@dicription
method1(...args:any) {
console.log('method1', args, this.method1)
}
}
console.log(ClassTest)
console.log(new ClassTest().method1(1,2,3))# 静态方法
除了类方法,装饰器也支持对静态方法的修改
// TODO: 此处扩展示例需修改
// 1. 定义类装饰器函数,传入为类构造器
function dicription2(target: any, key: String, val: PropertyDescriptor) {
console.log(`decrption method ${key}`, arguments);
}
class Class5 {
/**
* 2. 在方法之前采用 @<函数名> 设置装饰器
* 改代码会在运行时,效果如下
* class Class5 { method1() { // ... } };
* dicription(Class5,'method1',Object.getOwnPropertyDescriptor('method1') )
* */
@dicription2
static method1() {
console.log('method1')
}
}
console.log(Class5)
console.log(Class5.method1())TIP
注意静态方法装饰器的 target 为类,而非类的原型,一个简单的记忆原则是, target 为修改内容的所属者,例如方法属于类的原型所以 target 为类,静态方式属于类 所以 target 为类,该规则适用于所有装饰器
也可采用修改属性描述符,实现在调用时触发相关逻辑
// TODO: 此处扩展示例需修改
// 1. 定义类装饰器函数,传入为类构造器
function dicription3(target: any, key: PropertyKey , val: PropertyDescriptor) {
console.log('run in init', key, val);
let { value: method } = val;
function change(this: any) {
console.log('run in method call', this);
console.log(`decrption method ${key as string}`, arguments);
// 注意此处采用 call 防止 this 丢失
method.call(this, ...arguments)
}
// 修改原始函数
val.value = change
console.log('change method success', Object.getOwnPropertyDescriptor(target, key))
}
class Class6 {
@dicription3
static method1(...args:any) {
console.log('method1', args, this.method1)
}
}
console.log(Class6)
console.log(Class6.method1(1,2,3))# 属性装饰器
/**
* 注意由于属性值在实例化时才存在,所以无 value 参数
* @param target 类的原型,由于实例在初始化时才创建,此处用原型替代
* @param key 属性值
*/
function log(target:any, key: PropertyKey):any {
// 修改属性为只读
console.log('run in init');
let val: any;
return {
set: function (value: any) {
val = value;
console.log(`Set ${key as string} to ${value}`, this);
},
get: function() {
return val;
}
};
}
class Class8 {
@log
name: string;
constructor(name : string) {
this.name = name
}
}
let c = new Class8 ('tom');
console.log("%o", c)由于属性在实例化时才创建,而装饰器在类声明时就运行,如何实现在创建类时才执行描述符定义。
这里利用了 Reflect.decorate 方法。
TIP
注意定义属性装饰器时不要传入 value 参数, ts 检查会报错, 详见 Why I can't use decorator on property (opens new window)
# 静态属性装饰器
静态属性也可使用装饰器
function log(target:any, key: PropertyKey, descriptor: any): any{
console.log('run init', arguments)
let oldGetter = descriptor.get
return {
get: function () {
console.log('log', descriptor, this[key])
// 报错原因
oldGetter()
}
}
}
class Class10 {
get fullName():string {
return this.firstName + this.lastName
}
firstName: string;
lastName: string;
constructor (firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
}
// 对只读属性重写报错
let user = new Class10('hello','world');
user.fullName
# 访问器属性装饰器
也可设置访问器属性为装饰器。
function readonly(target:any, key: PropertyKey): any{
console.log('run init', arguments)
return {
writable: false
}
}
class Class9 {
@readonly
get name()
}
// 对只读属性重写报错
Class9.username = 'jack'
console.log(Class9.username)# 参数装饰器
参数装饰器,注意参数装饰器传入的第三个参数为 index, 参数对应索引值。
// 1. 定义参数装饰器
function requireVal(target: any,key: PropertyKey,value: any):any {
let d = Object.getOwnPropertyDescriptor(target,key)
console.log('init run', arguments, target, d)
Object.defineProperty(target, key, {
value: function() {
if(!arguments[value]) {
console.log('must have name')
}
return d?.value.apply(this,arguments)
}
})
}
class Greeter2 {
greet(@requireVal name?: string) {
return "Hello " + name;
}
}
// 运行时会检查属性值, TODO: tsnode 为什么失败
console.log(new Greeter2().greet())
console.log(new Greeter2().greet('tom'))# 装饰器工厂
利用装饰工厂动态创建装饰器,返回的装饰器会在类申明时运行。
// 1. 申明装饰器工厂返回装饰器函数
function testAble(flag: Boolean) {
return function(target: any) {
target._test = flag
}
}
// 返回类构造器,类申明时,设置类的 _test 为 false
@testAble(false)
class Class11 {
}
console.log(Class11)
// 返回类构造器,类申明时,设置类的 _test 为 true
@testAble(true)
class Class12 {
}
console.log(Class12)
# 装饰器组合
装饰器可以叠加使用。
function d1():any {
console.log("d1(): evaluated");
return function (
target: any,
key: PropertyKey,
val: PropertyDecorator
) {
console.log("d1(): called");
};
}
function d2():any {
console.log("d2(): evaluated");
return function (
target: any,
key: PropertyKey,
val: PropertyDecorator
) {
console.log("d2(): called");
};
}
/**
* tsc 编译后的执行结果类似
* let d1Res = d1()
* let d2Res = d1()
* // 靠近方法的装饰器先执行
* d2Res(target,key,value)
* d1Res(target,key,value)
*/
class Class14 {
@d1()
@d2()
method() {}
}
// return
// d1(): evaluated
// d2(): evaluated
// d2(): called
// d1(): called注意组合装饰器是的执行顺序是从内往外。
# 注解(Annotations) 和装饰器 (decorator)
参考 The difference between Annotations and Decorators (opens new window) 注解为类提供额外的注释信息,可以采用装饰器语法实现该功能。
# 使用场景
# 元编程
typescript 内置了一些装饰器通过 emitDecoratorMetadata 配置打开。
{
"compilerOptions": {
"emitDecoratorMetadata": true
}
}
该配置会内置 reflect-metadata (opens new window) 相关装饰器
# 总结
- 装饰器是一个函数,用于实现对类的动态修改,基本结构如下
function decorator(target, key ,value) { // 可以返回新的类或属性描述对象 } - 类的装饰器入参为类,支持返回新的类替换原有类
- 方法装饰器,入参为
类的原型,方法名,方法描述符,通过修改方法描述符实现运行时修改,支持返回新的属性描述符进行替换 - 静态方法装饰器,入参的第一个参数为类,其他功能和方法装饰器类似
- 属性装饰器,入参为
类的原型,属性名 - 静态属性装饰器,入参为
类的原型,属性名 - 访问器属性装饰器,入参为
类的原型,属性名 - 装饰器工厂,返回装饰器用于实现动态设置装饰器
- 多重装饰器,装饰器的叠加使用
- 使用场景
- 类装饰器,动态扩充方法,混入
- 属性装饰器,
- 格式化属性值
- 校验属性值
- 方法装饰器
- 废弃方法申明
- 方法只读
- 额外注意事项
- 装饰器的运行时机是在类申明后,可以通过修改属性描述符实现方法运行时的动态插入