速通TypeScript装饰器
一、简介
- 装饰器本质是一种特殊的函数它可以对:类、属性、方法、参数进行扩展,同时能让代码更简介。
- 装饰器自 2015 年在ECMAScript-6 中被提出到现在,已将近10年。
- 截止目前,装饰器依然是实验性特性,需要开发者手动调整配置,来开启装饰器支持。
- 装饰器有5种:
- 类装饰器
- 属性装饰器
- 方法装饰器
- 访问器装饰器
- 参数装饰器
备注:虽然 Typescript5.0 中可以直接使用 类装饰器 ,但为了确保其他装饰器可用,现阶段使用时仍建议使用 experimentalDecorators 配置来开启装饰器支持,而且不排除在未来的版本中,官方会进一步调整装饰器的相关语法!
参考:Announcing TypeScript 5.0 RC - TypeScript
二、类装饰器
1、基础语法
类装饰器是一个应用在类声明上的函数,可以为类添加额外的功能,或添加额外的逻辑。
第一步:开启支持装饰器
第二步:入门基础语法
// 装饰器函数
/*
Demo函数会在 Person类 定义时执行
参数说明:o target参数是被装饰的类,即:Person
*/
function Demo(target: Function) {console.log(target);
}@Demo // 等价于 执行了 Demo 函数 ,还将 Person 传进去调用了 Demo(Person)
class Person{constructor(public name:string, public age:number){}greet(){console.log(`Hi, I am ${this.name}`)}
}
2、应用举例
需求:定义一个装饰器,实现 Person 实例调用 tostring 时返回 JS0N.stringify 的执行结果。
function CustomString(target: Function) {// 通过 target 获得属性, 添加 toString 属性target.prototype.toString = function(){return JSON.stringify(this)}// 封锁 target 属性 ,不允许更改Object.seal(target.prototype)
}@CustomString
class Person{constructor(public name:string, public age:number){}greet(){console.log(`Hi, I am ${this.name}`)}
}const p1 = new Person('Angindem', 30)
console.log(p1.toString());/*// @ts-ignore : 忽略类型检查
*/
interface Person{x:number
}
Person.prototype.x = 99
3、关于返回值
类装饰器有返回值:若类装饰器返回一个新的类,那这个新类将替换掉被装饰的类。
类装饰器无返回值:若类装饰器无返回值或返回 undefined ,那被装饰的类不会被替换。
function Demo(target:Function) {return class {test() {console.log('200');console.log('300');console.log('400');}}
}@Demo
class Person{test() {console.log('test');}
}console.log(Person);
4、关于构造类型
在 TypeScript 中, Function 类型所表示的范围十分广泛,包括:普通函数、箭头函数、方法等等但并非 Function 类型的函数都可以被 new 关键字实例化,例如箭头函数是不能被实例化的,那么TypeScript 中概如何声明一个构造类型呢?有以下两种方式:
仅声明构造类型
/*new 表示:该类型是可以用new操作符调用。...args 表示:构造器可以接受【任意数量】的参数。 any[] 表示:构造器可以接受【任意类型】的参数。{} 表示:返回类型是对象(非nu11、非undefined的对象)。*/type Constructor = new (...args: any[]) => {};
// 需求是 fn 的是一个类
function test(fn: Function) {}
const Person = () => {}test(Person)
声明构造类型+指定静态属性
/*new 表示:该类型是可以用new操作符调用。...args 表示:构造器可以接受【任意数量】的参数。 any[] 表示:构造器可以接受【任意类型】的参数。{} 表示:返回类型是对象(非nu11、非undefined的对象)。*/type Constructor = {new(...args: any[]): {};wife: string;
}
// 需求是 fn 的是一个类
function test(fn: Constructor) {}class Person {static wife:string
}test(Person)
5、替换被装饰的类
对于高级一些的装饰器,不仅仅是覆盖一个原型上的方法,还要有更多功能,例如添加新的方法和状态。
需求:设计一个 LogTime 装饰器,可以给实例添加一个属性,用于记录实例对象的创建时间,再添加一个方法用于读取创建时间。
// 声明构造器 可以 new 的 任意数量 ,任意类型 ,返回值 非 null 和 undefined
type Constructor = new (...args: any[]) => {}// 自动合并,为 class 类里面声明我们新添加的方法
interface Person{getTime():void
}// <T extends Constructor> 表示我们的 泛型 target 类 必须有一个 构造器
// class extends target 新的类 保留原有的类基础上 ,我们添加 getTime 方法,以及构造时新增记录构造时间
function LogTime<T extends Constructor>(target:T) {return class extends target{createdTime: Dateconstructor(...args: any[]) {super(...args)this.createdTime = new Date()}getTime():string{return `This instance was created at ${this.createdTime}`}}
}@LogTime
class Person{constructor(public name:string, public age:number){}speak():void{console.log(`I am ${this.name} and I am ${this.age} years old.`)}
}const p1 = new Person('Angindem', 30)
console.log(p1.getTime());
三、装饰器工厂
装饰器工厂是一个返回装饰器函数的函数,可以为装饰器添加参数,可以更灵活地控制装饰器的行为。
需求:定义一个 LogInfo 类装饰器工厂,实现 Person 实例可以调用到 introduce 方法,且 introduce 中输出内容的次数,由 LogInfo 接收的参数决定
interface Person {introduce():void
}
// 装饰器 需要 返回一个装饰器 所以我们 直接 @LogInfo(3) ,但是 缺少了 target 类值
// 所以我们可以 嵌套 一个函数,最后返回装饰器. 整个过程步骤,外部的就是 装饰器工厂,最后返回的是 装饰器
function LogInfo(count:number) {return (target: Function) => { target.prototype.introduce = function () {for (let i = 0; i < count; i++) {console.log(`I am ${this.name}, I am ${this.age} years old.`)}}}
}
@LogInfo(3)
class Person{constructor(public name: string, public age: number) {}speak() {console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);}
}
const p1 = new Person('Angindem', 30);
p1.introduce();
四、装饰器组合
装饰器可以组合使用,执行顺序为:先【由上到下】的执行所有的装饰器工厂,依次获取到装饰器,然后再【由下到上】执行所有的装饰器。
执行顺序(先 【由上到下】 工厂 ,后 【由下到上】 装饰器 )
// 装饰器
function test1(target: Function) {console.log('test1');
}// 装饰器工厂 2
function test2<T>(params?: T) {console.log('test2工厂');return function (target: Function) {console.log('test2');if (params) {console.log(params);}}
}// 装饰器工厂 3
function test3<T>(params?: T) {console.log('test3工厂');return function (target: Function) {console.log('test3');if (params) {console.log(params);}}
}// 装饰器
function test4(target: Function) {console.log('test4');
}// 像 洋葱模型 一样, 先 按顺序 执行工厂 ‘拨开’ ,最后由 下到上 执行装饰器
@test1
@test2()
@test3()
@test4
class Test { }
应用
type Constructor = new (...args: any[]) => {};
interface Person{getTime(): string;introduce(): void;
}
// 装饰器
function CustomString(target: Function) {target.prototype.toString = function(){return JSON.stringify(this)}Object.seal(target.prototype)
}// 装饰器工厂
function LogInfo(count:number) {return (target: Function) => { target.prototype.introduce = function () {for (let i = 0; i < count; i++) {console.log(`I am ${this.name}, I am ${this.age} years old.`)}}}
}
// 装饰器
function LogTime<T extends Constructor>(target:T) {return class extends target{createdTime: Dateconstructor(...args: any[]) {super(...args)this.createdTime = new Date()}getTime():string{return `This instance was created at ${this.createdTime}`}}
}@CustomString
@LogInfo(5)
@LogTime
class Person{constructor(public name:string, public age:number){}speak(){console.log(`Hi, I am ${this.name}`)}
}
const p1 = new Person('Angindem', 30)
p1.speak();
p1.introduce();
console.log(p1.toString());
console.log(p1.getTime());
五、属性装饰器
1、基础语法
/**参数说明:o target:对于静态属性来说值是类,对于实例属性来说值是类的原型对象o propertykey:属性名。
*/
function Demo(target: object, propertKey: string) {console.log(target, propertKey);
}class Person{@Demo name: string@Demo age: number@Demo static school: stringconstructor(name:string,age:number) {this.name = namethis.age = age}speak(){console.log(`Hi, I am ${this.name}`)}
}
2、关于属性遮蔽问题
如下代码中:当构造器中的 this.age = age 试图在实例上赋值时,实际上是调用了原型上 age 属性的 set 方法。
class Person{name: stringage: numberconstructor(name:string,age:number) {this.name = namethis.age = age}
}// // 这里 构造后 触发 查找一次 age 赋值,
// const p1 = new Person('Angindem', 18)// 因为先构造原因, Object.defineProperty 上的 age 值 为 130,并没有收到赋值 18
// let value = 130
// Object.defineProperty(Person.prototype, 'age', {
// get() {
// return value
// },
// set(newValue) {
// value = newValue
// }
// })let value = 130
Object.defineProperty(Person.prototype, 'age', {get() {return value},set(newValue) {value = newValue}
})// 这里 构造后 触发 查找 age 赋值, 同时将 Object.defineProperty 上的 age 赋值了
const p1 = new Person('Angindem', 18)
console.log(p1);
3、应用举例
需求:定义一个 state 属性装饰器,来监视属性的修改。
function Status(target:object,propertyKey:string) {let key = `__${propertyKey}__`Object.defineProperty(target,propertyKey,{get: function() {return this[key]},set: function (newValue) {if(key !== newValue) console.log(`${propertyKey} is changed from ${key} to ${newValue}`);this[key] = newValue},enumerable: true, // 可枚举性: 循环遍历的时候参不参与遍历configurable: true // 可配置性:: 是否可删除})
}class Person{name: string@Status age: numberconstructor(name:string,age:number) {this.name = namethis.age = age}
}const p1 = new Person('Angindem', 20)
const p2 = new Person('Angindem', 45)
p1.age = 21
console.log(p1);
console.log(p2);
六、方法装饰器
1、基础语法
/*参数说明:o target:对于静态方法来说值是类,对于实例方法来说值是原型对象o propertykey:方法的名称。o descriptor:方法的描述对象,其中value属性是被装饰的方法。
*/
function Demo(target: any, propertyKey: string, descriptor: PropertyDescriptor) {console.log(target, propertyKey, descriptor);}class Person{name: stringage: numberconstructor(name:string,age:number) {this.name = namethis.age = age}@Demospeak() {console.log(`${this.name} is ${this.age} years old`)}@Demostatic isAdult(age:number) {return age >= 18}
}
2、应用举例
function Logger(target: object, propertyName: string, descriptor: PropertyDescriptor) {// 存储原始方法const original = descriptor.value// 替换原始方法descriptor.value = function (...args:any[]) {console.log(`${propertyName} 开始执行......`);// const res = original.call(this, ...args)const res = original.apply(this, args) // 经典面试题,注意apply和call的区别console.log(`${propertyName} 执行完毕......`);return res;}
}class Person{name: stringage: numberconstructor(name:string,age:number) {this.name = namethis.age = age}@Logger speak(str:string) {console.log(`${this.name} is ${this.age} years old ,${str}`)}static isAdult(age:number) {return age >= 18}
}const p1 = new Person('Angindem', 18)
p1.speak('你好')
七、访问器装饰器
1、基础语法
/*
参数说明:○ target:1. 对于实例访问器来说值是【所属类的原型对象】。2. 对于静态访问器来说值是【所属类】。○ propertyKey:访问器的名称。○ descriptor: 描述对象。
*/
function Demo(target: object, propertyKey: string, descriptor: PropertyDescriptor) {console.log(target)console.log(propertyKey)console.log(descriptor)
}
class Person {@Demoget username() {return 'Angindem'}@Demostatic get grade() {return 'B-324'}
}
2、应用举例
需求:对 Weather 类的 temp 属性的 set 访问器进⾏限制,设置的最低温度 -50 ,最⾼温度 50
// 装饰工厂
function RangeValidate(min: number, max: number) {return function (target: object, propertyKey: string, descriptor: PropertyDescriptor) {// 保存原始的 setter ⽅法,以便在后续调⽤中使⽤const originalSetter = descriptor.set;// 重写 setter ⽅法,加⼊范围验证逻辑descriptor.set = function (value: number) {// 检查设置的值是否在指定的最⼩值和最⼤值之间if (value < min || value > max) {// 如果值不在范围内,抛出错误throw new Error(`${propertyKey}的值应该在 ${min} 到 ${max}之间!`);}// 如果值在范围内,且原始 setter ⽅法存在,则调⽤原始 setter ⽅法if (originalSetter) {originalSetter.call(this, value);}};};
}
class Weather {private _temp: number;constructor(_temp: number) {this._temp = _temp;}// 设置温度范围在 -50 到 50 之间@RangeValidate(-50, 50)set temp(value) {this._temp = value;}get temp() {return this._temp;}
}
const w1 = new Weather(25);
console.log(w1)
w1.temp = 67
console.log(w1)
八、参数装饰器
1、基础语法
/*
参数说明:○ target:1.如果修饰的是【实例⽅法】的参数,target 是类的【原型对象】。2.如果修饰的是【静态⽅法】的参数,target 是【类】。○ propertyKey:参数所在的⽅法的名称。○ parameterIndex: 参数在函数参数列表中的索引,从 0 开始。
*/
function Demo(target: object, propertyKey: string, parameterIndex: number) {console.log(target)console.log(propertyKey)console.log(parameterIndex)
}
// 类定义
class Person {constructor(public name: string) { }speak(@Demo message1: any, mesage2: any) {console.log(`${this.name}想对说:${message1},${mesage2}`);}
}
2、应用举例
需求:定义⽅法装饰器 Validate ,同时搭配参数装饰器 NotNumber ,来对 speak ⽅法的参数类型进⾏限制。
function NotNumber(target: any, propertyKey: string, parameterIndex: number) {// 初始化或获取当前⽅法的参数索引列表let notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || [];// 将当前参数索引添加到列表中notNumberArr.push(parameterIndex);// 将列表存储回⽬标对象target[`__notNumber_${propertyKey}`] = notNumberArr;
}
// ⽅法装饰器定义
function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {const method = descriptor.value;descriptor.value = function (...args: any[]) {// 获取被标记为不能为空的参数索引列表const notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || [];// 检查参数是否为 null 或 undefinedfor (const index of notNumberArr) {if (typeof args[index] === 'number') {throw new Error(`⽅法 ${propertyKey} 中索引为 ${index} 的参数不能是数字!`)}}// 调⽤原始⽅法return method.apply(this, args);};return descriptor;
}
// 类定义
class Student {name: string;constructor(name: string) {this.name = name;}@Validatespeak(@NotNumber message1: any, mesage2: any) {console.log(`${this.name}想对说:${message1},${mesage2}`);}
}
// 使⽤
const s1 = new Student("张三");
s1.speak(100, 200);