TypeScript 装饰器(Java中的注解)
开始
开启装饰器:
在 tsconfig.json 文件中加入配置项,目前装饰器为实验性功能,只有类装饰器可以在不开启情况下使用,其它位置使用装饰器需要开启才能使用。
{
"target": "es6",
"experimentalDecorators": true
}
装饰器实际上就是一个函数方法体,我们可以通过声明一个函数方法体,做某些操作,这些操作将在其它的类,方法中加以增强。
类的装饰器
基本语法定义:
- 声明一个方法,这个方法将是用于类的装饰器上的
- 这个装饰器方法需要声明一个参数,这个参数将会接收使用它的类的类型
代码:
/**
* 定义一个用于装饰器的方法
* @param target 作为装饰器来使用它的对象
* @constructor
*/
function Demo(target:Function):void {
}
/**
* 把装饰器放在类上
*/
@Demo
class Person {
name:string;
age:number;
constructor(name:string,age:number) {
this.name = name;
this.age = age;
}
}
案例:通过增强toString方法使得对象的 toString 方法转为 Json 数据
/**
* 定义一个装饰器,并重写toString方法为 Json转化
* @param target
* @constructor
*/
function CustomToString(target: Function): void {
// 把 toString 重写成 JSON.stringify
target.prototype.toString = function () {
return JSON.stringify(this);
}
}
/**
* 在类方法中使用装饰器,这个类的 toString 方法将被改写
*/
@CustomToString
class Person {
constructor(public name: string, public age: number) {
}
}
const p = new Person("tom", 18);
// 这里会输出 {"name":"tom","age":18} 而不是 [object object]
console.log(p.toString());
对于装饰器的返回值
- 装饰器本身就是个函数方法,所以方法必定会有返回值的
- 如果装饰器有返回值,则返回的内容将替代被装饰的类本身的类型
- 如果装饰器没有返回值,则不会替代被装饰的类的类型数据。
案例:为一个类增加实例化时间,在创建初就内置一个实例化时间的值
思路:
- 1.我们可以使用装饰器方法的返回值替换原对象的原理,在装饰器方法中声明一个继承原类的类
- 2.在继承的类中新增一个用于计算实例化时间的成员变量 如 createTime
- 3.在构造方法中取出时间并给这个成员变量 createTime
问题:如何在装饰器方法中继承原类,原类的成员变量如何被继承
- 我们可以使用 ...args:any[] 的方法,来接收原类在构造时传入的参数
- 并在构造函数中调用 super,可以使用 ...args 来解构参数数组
代码如下:
/**
* 定义一个用于约束能new 构造的类型
*/
type CustomType = new(...args: any[]) => object;
/**
* 定义一个装饰器
* @param target
* @constructor
*/
function CustomConstruct<T extends CustomType>(target: T) {
/**
* 对原有的类进行继承并增加一个字段 createTime
* class 类名可以不定义,也可以定义名字
* 如 class name extends target
* 注意因为这个是装饰器,所以会传入被装饰的类 target
* 我们需要继承这个 target
*/
return class extends target {
// 原有的类进行继承并增加一个字段 createTime
createTime: Date;
constructor(...args: any[]) {
super(...args);
this.createTime = new Date();
}
}
}
/**
* 在类方法中使用装饰器,这个类的 toString 方法将被改写
*/
@CustomConstruct
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
关于上面 使用 泛型 T 来extends 类型,而不是直接在 target: CustomType 中定义 的原因说明:
- CustomType 是定义了一个类型,而 Person 它是一个类,也是一个类型
- 如果我们说 typeof CustomType = CustomType,那么
- type Person = Person
- type CustomType 并不等于 typeof Person
- 因为类也是一种类型,Person 就是 Person 类,CustomType 也算是一种类型
- 我们不能粗略的把 Person 类和 CustomType 类划等号,因为Person对象可以比 CustomType 有更多功能
- 也就是说,Person 的对象可以既实现了 CustomType 的要求,也有自己的另外功能
- 那直接把 Person 的实例对象当成 CustomType 要求,就不对了
- 这是为什么要使用 <T extends CustomType> 而不直接 target: CustomType 的原因
- 当然,使用 target: CustomType 也是可以的(不过会报错),因为本例中 Person 就是满足 CustomType 的要求
- 但是强行使用 target: CustomType 后,被装饰的 Person 类就强行变成了 CustomType 类了
- 为了保证 Person 类能被 CustomType 约束,但同时又不丢失 Person 的类型信息 (原本Person变CustomType了)
- 所以使用泛型来定义某个类是 CustomType 的子类,保留了 Person 原来的类型信息(泛型T记录了)
- 对于 Function 为什么可以直接定义,是因为 Function 是一个官方定义的泛类,就和 any 一样
- 对于自定义类来说,无法判断自定义类一定就是类型约束(就像子类一定就是父类是不对的)
- 举个例子:我们创建了一个大数类 BigNumber(本例的Person,或泛型T),BigNumber 按照 Number 类(本例的CustomType)的所有规范实现了方法(对照本例的类型约束声明,类型约束声明不是继承),也只能说 BigNumber 按照 Number 的规范完全做了一个类。但我们不能粗略的把BigNumber 就是 Number了,因为BigNumber有自己的功能,用 Number 来定义 BigNumber 就会使 BigNumber 丢失自己的功能信息了。
明确一点,CustomType 是一个类型规范,Person 可以在满足 CustomType 的规范情况下还能有更多功能,但在接收参数时直接定义 Person 实例对象就是 CustomType ,会使 Person 对象强转成 CustomType ,不但使 Person 实例对象的类型强转成 CustomType ,而且会把 Person 自己该有的功能也会丢失。.这在 TS 中并不规范。
之所以 能用 <T extends CustomType> 是因为,target 接收到的是 Person 的实例对象,它应该是 CustomType 类型的子类型(即满足CustomType的类实例)
(Java 中好像是允许父类包子类对象的情况,但是这样会丢失子类的功能)
关于定义装饰器中的target类型的问题
在上面我们声明一个装饰器方法时,需要定义一个参数,这个参数是用来接收被装饰的类的对象的,但是我们在这里使用了 Function 类型:
function Demo(target:Function):void { }
对于这个 Function 类型,它能函括的类型非常多,比如“函数方法”,“箭头函数方法”,“类”等。
但是并非所有函数都可以被 new 关键字实例化,例如“箭头函数”都不允许被实例化,但是 Function 类型都包含在内了,这使得在约束 target 的类型时不够严谨,所以我们建议使用能严谨约束可以被 new 实例化的类型。
此种类型在TS中不存在,但是我们可以使用 type 关键字来声明自定义类型。
声明方法如下:
type CustomType = new (...args: any[]) => {}
声明说明如下:
- type 是一ts 中用于声明类型的关键字,可以声明自定义类型
- new 是指这个类型是【可被实例化的】
- (...args) 是指多重参数接收,实例化时可以无限接收多个参数,会被记录到 args 中,args 为一个多类型数组,而在这里则表示,声明了一个可允许任意个参数的类型
- any[] 是指 args 接收到的是一个任意数据类型的数组,在这里刚表示,声明了一个可允许存任意数据类型参数的类型
- {} 这里是指这个类型约束返回的是一个对象类似于返回值类型定义为 object
除了上面这种声明,是代表 【我要声明一个类型,这个类型是可new实例化的。。等等】
在声明类型上,还可以有更多操作,比如类型要求这个类还需要符合有静态类型的,比如下面这样定义:
/**
* 把类型要求写进一个 {} 中,因为还需要添加其它类型要求
*/
type CustomType = {
// 在类的格式中定义多个类型要求
new(...args: any[]): object;
/**
* 这里是指类除了需要符合上面的实例化要求外,还需要符合带有下面的静态成员变量
*/
wife: string;
}
/**
* 声明一个类,类是可以支持实例化的,也支持传递多个参数,且返回的是一个对象
* 但类型中还包含了静态类型,所以这个类也必须满足类型要求增加静态类成员
*/
class Student {
static wife: string = "老婆";
}
function test(fn:CustomType){
}
test(Student)
装饰器工厂
装饰器工厂是一个返回装饰器函数的函数,可以为装饰器添加参数,可以更灵活地控制装饰器的行为。
通俗的讲:
- 装饰器 @Xxxx 其实就是调用装饰器方法 function Xxxx(object)
- object 是把被装饰的类的对象
- 所以,当装饰器 @Xxxx(abc) 时其实就是调用 装饰器方法的 function (abc)(object)
- 就是传入abc调用了方法,方法返回一个方法,再调用返回的方法,传入 object
- 所以,装饰器工厂,实际上就是一个返回 装饰器方法 的方法
- 在方法中返回一个方法,返回的那个方法才是真正的装饰器方法
代码如下:
/**
* 声明一个装饰器工厂,它允许传入一个自定义对象,name 和 age
* 并返回一个装饰器方法,装饰器方法才是接收被装饰对象
* @param p 自定义对象
* @constructor
*/
function Info(p: { name: string, age: number }) {
/**
* 这一层才是一个装饰器方法
*/
return function (target: Function) {
/**
* 使被装饰的类添加一个 abc() 的方法
*/
target.prototype.abc = function (): string {
return `我的名字是${p.name},我的岁数是 ${p.age}`
}
}
}
/**
* 把装饰器用在类上
* 装饰器工厂的参数就可以在这里传入
*/
@Info({name: "小明", age: 18})
class MPerson {}
/**
* 对于TS来说,MPerson 并没有 abc() 方法,
* 所以调用时会报错(但是会成功调用,因为装饰器添加了)
* 为了解决 TS 报错问题,可以使用 interface 的自动追加类型特性,显式的为 MPerson 类
* 添加一个 abc() 方法的声明
*/
interface MPerson {
abc(): string;
}
/**
* 此时调用 abc() 就能应用装饰器中的方法了
*/
const p = new MPerson();
p.abc(); // 我的名字是小明,我的岁数是18
装饰器组合
当一个类中,应用了多个装饰器时,这些装饰器都会被依次调用,但是调用顺序有所不同。
如这样的代码:
@Test1
@Test2()
@Test3()
@Test4
class LPerson {}
上面的代码一共使用了4个装饰器,其中 Test2 和 Test3 是装饰器工厂
顺序如下:
- Test2 工厂被触发
- Test3 工厂被触发
- Test4 被触发
- Test3 被触发
- Test2 被触发
- Test1 被触发
由上可见,当存在装饰器工厂时,会优选调用装饰器工厂生成装饰器,其顺序是从上到下执行
当所有的装饰器工厂执行完成后,会从离被装饰类最近的装饰器开始调用,也就是顺序是从下到上执行
属性装饰器
属性装饰器是用于装饰属性的,它应该应用在属性的上方或左侧方
装饰器会收到两个参数
- 第一个是属性所在的类实例
- 当装饰器在实例成员时,它收到的就是实例对象
- 当装饰器在静态成员时,它收到的就是类本身
- 第二个是被装饰的属性的 key 名
示例代码如下:
/**
* 定义一个装饰器,用在属性上
* 它会接收到两个参数,
* - 参数一:实例对象本身 || 类本身 target:object
* - 参数二:被装饰的属性的key名称 propertyKey:string
*/
function Demo(target: object, propertyKey: string) {
console.log(target, propertyKey)
}
class LPerson {
name: string;
// 把装饰器放在属性上
@Demo age: number;
@Demo static school: string;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
关于属性遮蔽的问题
在 JS 中,有一个方法可以动态的往对象实例中添加属性,比如
// 实例化一个对象
const per = new LPerson("小明", 18)
let value = ""
Object.defineProperty(per, "like", {
get(): any {
return value;
},
set(val: any) {
value = val
}
})
但当使用 Object.defineProperty 去定义类中已有的属性时,类中定义的同名属性将会被覆盖
比如下面代码,覆盖了 LPerson 类中的 age 属性
class LPerson {
name: string;
age: number;
static school: string;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
/**
* 使用原型方法,动态的往一个对象中添加一个属性
*/
let value = ""
Object.defineProperty(LPerson.prototype, "age", {
get(): any {
return value;
},
set(val: any) {
value = val
}
})
// 实例化一个对象
const per = new LPerson("小明", 18)
那么这时当 LPerson 在实例化过程中,LPerson 原先的 属性 age 将会被 Object.defineProperty 中定义的 age 覆盖并实例,实际操控的将是 Object.defineProperty 的 age 而不是 LPerson 原先的 age值了。
属性装饰器的应用
通过上面的 属性覆盖 的原理,我们就可以在装饰器中使用 Object.defineProperty 来覆盖原有的属性,并接管原有属性的控制
function Demo(target: object, propertyKey: string) {
let key = `__${propertyKey}`
Object.defineProperty(target, propertyKey, {
get(): any {
return this[key];
},
set(val: any) {
... 可做一些响应式或其它操作
this[key] = val
}
})
}
为了防止多实例互相影响临时存储变量key,所以使key不作为直接存储数据的变量,而是作为存储在对象中的key的名字,通过 this[key] 来获取当前对象的 key 的值。
方法装饰器
方法装饰器应用在类的方法上,可以定义在实例方法中,也可以定义在静态方法中,装饰器接收三个参数:
- 参数一,target: object 对于静态方法来说值是类,对于实例方法来说值是原型对象
- 参数二,propertyKey: string 方法的名称
- 参数三,descriptor: PropertyDescriptor 方法的描述对象,其中 value 属性是被装饰的方法
应用:在一个方法的前后加上日志输出
/**
* 定义一个装饰器,使得被装饰的对象带有日志功能
* @param target 对象实例 或 类
* @param propertyKey 方法名
* @param descriptor 方法的一些描述数据
* @constructor
*/
function Logger(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
/**
* 思路方法:
* 1.先把原来的方法取出来保存(在descriptor.value中保存了方法自身)
* 2.覆盖原有的方法逻辑
* 3.在覆盖的方法中加入日志功能,并调用原来的方法
* 3.1 注要不要直接调用,否则方法会丢法this指向
* 3.2 所以需要使用 call 或 apply 来调用方法执行
* 3.3 如果方法有参数,应该接收参数
* 4.把执行结果保存下来
* 5.返回执行结果
*/
// 保存原有的方法
const fun = descriptor.value;
descriptor.value = function (...args:any[]) {
console.log(`${propertyKey}方法开始执行……`)
let result = fun.call(this,...args);
// 也可以使用 apply: apply 接收的参数是一个参数数组,call 是拆分每个参数传入
// fun.apply(this,args)
console.log(`${propertyKey}方法执行结束……`)
// 返回执行结果
return result;
}
}
class KPerson {
constructor(public name: string, public age: number) {
}
/**
* 把装饰器应用在方法上
*/
@Logger
doSpeak(): void {
console.log(`我叫 ${this.name},我今年 ${this.age} 岁`)
}
}
访问器装饰器
对于一些在类中的私有成员变量,我们给这些私有成员变量设置 get 和 set 方法
- 参数一,target: object 对于静态方法来说值是类,对于实例方法来说值是原型对象
- 参数二,propertyKey: string 方法的名称
- 参数三,descriptor: PropertyDescriptor 方法的描述对象,其中 get 和 set 属性是被装饰的方法的 get set
比如我定一个 Weather 类,接收一个温度属性:
class Weather {
private _temp: number;
constructor(_temp: number) {
this._temp = _temp;
}
get temp() {
return this._temp;
}
set temp(val: number) {
this._temp = val;
}
}
此时我希望在设置温度中增加一个验证装饰器,用于验证设置的温度是否合法,这时我们需要使用装饰器工厂,来定义温度区间:
function WeatherValidation(low: number, height: number) {
return function (target: object, propertyKey: string, descriptor: PropertyDescriptor) {
/**
* 保存原来的set方法
*/
const setFun = descriptor.set
/**
* 创建一个新的方法覆盖原来的set方法
* @param value
*/
descriptor.set = function (value: number) {
// 如果有问题就抛出异常
if (value < low || value > height) {
throw new Error(`温度只能控制在 ${low} 和 ${height} 之间`);
}
// 如果值没问题就set值
if (setFun) {
setFun?.call(this, value)
}
}
}
}
class Weather {
private _temp: number;
constructor(_temp: number) {
this._temp = _temp;
}
get temp() {
return this._temp;
}
// 装饰器工厂设置参数
@WeatherValidation(-10, 100)
set temp(val: number) {
this._temp = val;
}
}
参数装饰器
对于一些在类中的方法中需要传递的参数,我们给这些参数中定义装饰器
- 参数一,target: object 对于静态方法来说值是类,对于实例方法来说值是原型对象
- 参数二,propertyKey: string 方法的名称
- 参数三,parameterIndex: number 参数在函数参数列表中的索引 , 从0开始
共有 0 条评论