所谓的装饰器, 实际上可以看作就是一个函数, 一个可以扩展类及其成员功能的函数! 在使用Typescript之前, 记得开启experimentalDecorators设置, 本文中使用的Typescript版本为5.0.2;
装饰器分类
类装饰器
前面说了, 装饰器本质上是一个函数, 所谓的类装饰器, 就是用来修饰整个类的函数, 并从整个类的级别上去扩展功能, 它的参数只有一个, 那就是被修饰的类本身! Typescript内置了ClassDecorator, 表示类装饰器的类型; 后续还有PropertyDecorator、MethodDecorator、ParameterDecorator分别代表属性装饰器、方法装饰器和参数装饰器的类型; 我们先来看看类装饰器的使用吧:
// 为类的原型新增一个方法
function AddMethod ():ClassDecorator {
return (target:any) => {
target.prototype.fn = () => {
console.log('装饰器内的方法')
}
}
}
// 为类添加静态方法
function AddProperty (value:string):ClassDecorator {
return (target:any) => {
target.staticProperty = value
}
}
@AddProperty('装饰器静态属性')
@AddMethod()
class Foo {
static staticProperty: any
fn() {
throw new Error("Method not implemented.")
}
}
let foo = new Foo()
foo.fn() // 装饰器内的方法
console.log(Foo.staticProperty) // 装饰器静态属性
除了拓展属性/方法, 类装饰器还能直接返回一个新的子类, 这个子类则继承了被修饰的那个类, 而被修饰的类, 最终会被这个子类所代替;
// 重写一个类的装饰器
function OverrideClass ():ClassDecorator {
return (target:any) => {
return class extends target {
print () {
console.log('this is new print')
}
}
}
}
@OverrideClass()
class Bar {
print () {
console.log('this is old print')
}
}
// 注意, 这里的Bar已经不是原来那个了, 而是装饰器中返回的那个子类
const bar = new Bar()
bar.print() // this is new print
方法装饰器
虽然, 使用类装饰器可以直接为类新增方法和属性, 但是, Typescript为我们提供了专门处理方法/属性的装饰器, 我们就来先认识下方法装饰器, 不同于类装饰器, 它的入參有三个, 分别为类原型对象、方法名、方法的属性描述符, 显然, 相比于类装饰器, 它提供的信息更多, 应对的场景也更具体!
// 计算一个异步函数的执行时间
function calcReqTime ():MethodDecorator {
return (_target:any, methodName, descriptor: TypedPropertyDescriptor<any>) => {
const originMethod = descriptor.value
descriptor.value = async () => {
console.time(String(methodName) + '请求时间')
await originMethod()
console.timeEnd(String(methodName) + '请求时间')
}
}
}
class Axios {
@calcReqTime()
post ():Promise<void> {
return new Promise((resolve) => {
setTimeout (() => {
console.log('请求成功')
resolve()
}, 3000)
})
}
}
const axios = new Axios()
axios.post()
/**
* 请求成功
post请求时间: 3.006s
*/
我们通过descriptor.value, 获取到被修饰的方法的具体逻辑, 然后将其赋给originMethod变量, 然后再重新为descriptor.value赋值, 即重新定义其逻辑, 增加了计算执行时间的console.time代码, 再执行originMethod, 就能够得到其执行的时间了; 以上的代码其实就是在维持方法原有功能基础上, 扩展了类方法的能力; 还有一点要注意, 这里所说的方法, 其实是类原型上的方法, 而非类上的静态方法, 如果想要为一个类添加静态方法, 还是需要使用类装饰器;
访问符装饰器
其实, 所谓的访问符装饰器就是方法装饰器, 只不过, 刚才的方法装饰器里我们操作了descriptor.value, 现在我们操作descriptor.set, 当然, 也可以是descriptor.get, 所以, 你也可以说这就是方法装饰器, 只是换了个叫法
class House {
total:number = 0
constructor(public length:number, public width:number) {}
get area () {
return this.length * this.width
}
@logPrice('王老五')
set price (value: number) {
this.total = value * this.area
}
}
function logPrice (name:string): MethodDecorator {
return (_target: any, methodKey: any, descriptor: TypedPropertyDescriptor<any>) => {
const originSetter = descriptor.set
descriptor.set = function (value:number) {
if (originSetter) {
originSetter.call(this, value)
console.log(`${name} will spent ${value} per square meters to buy the house`)
}
}
}
}
const house = new House(100, 100)
house.price = 200000 // 王老五 will spent 200000 per square meters to buy the house
属性装饰器
不同于方法装饰器, 属性装饰器只有两个参数: 类的类原型对象、属性名称, 因为在类定义阶段, 还无法确定属性的值, 所以, 我们能做的, 也就只是为一个类的原型添加/修改属性了
function PropDeco (value:any):PropertyDecorator {
return (prototype: any, propName: string|symbol) => {
prototype[propName] = value
prototype.desc = `我的名字叫${value}`
}
}
class Info {
@PropDeco('大名')
name:string|undefined
desc: any
}
const info = new Info()
console.log(info.name, 'info.name') // 大名
console.log(info.desc, 'info.desc') // 我的名字叫大名
参数装饰器
参数装饰器接受三个参数: 类型原型对象、方法的名称、被修饰参数的索引值
function ParamsDeco(): ParameterDecorator {
return (prototype: any, methodName: string | symbol, index: number) => {
console.log(prototype, methodName, index);
};
}
class Info {
getData(@ParamsDeco() id: number, name: string) {}
}
new Info(); // {} getData 0
装饰器执行顺序
那么, 这几种装饰器的执行顺序是怎样的呢?
不同装饰器执行顺序
我们要先明白一个点: 即使不对被修饰的class进行实例化或者其他操作, 装饰器也会执行, 或者说, 你的class啥也不做, 光定义了一个类, 只要上面有装饰器, 那么, 这些装饰器其实就已经执行了, 因为装饰器本就是一个方法, 它是来修饰或者说辅助class的, 但是class何时实例化与其并无关系, 相当于就是各干各的
function ParamsDeco(): ParameterDecorator {
console.log('ParaDeco执行');
return (prototype: any, methodName: string | symbol, index: number) => {
console.log('ParaDeco应用');
};
}
function Cls() {
console.log('Cls执行');
return (target: any) => {
console.log('Cls应用');
};
}
function Met(): MethodDecorator {
console.log('Met执行');
return (target: any, method: string | symbol, descriptor: any) => {
console.log('Met应用');
};
}
function Prop(): PropertyDecorator {
console.log('Prop执行');
return (target: {}, propName: string | symbol) => {
console.log('Prop应用');
};
}
@Cls()
class Info {
@Prop()
name: any;
@Met()
getData(@ParamsDeco() id: number, name: string) {}
}
/**
*
Prop执行
Prop应用
Met执行
ParaDeco执行
ParaDeco应用
Met应用
Cls执行
Cls应用
*/
我们将属性和方法调换一下:
@Cls()
class Info {
@Met()
getData(@ParamsDeco() id: number, name: string) {}
@Prop()
name: any;
}
/**
Met执行
ParaDeco执行
ParaDeco应用
Met应用
Prop执行
Prop应用
Cls执行
Cls应用
*/
由此可知, 装饰器的执行顺序上
- 方法和属性的执行/应用顺序, 取决于其放置的位置;
- 参数装饰器在其所在方法执行后/应用之前被执行并应用;
- 类装饰器一定在最后被执行并应用;
相同装饰器执行顺序
前面展示了不同装饰器的执行顺序, 接下来要讨论的, 是相同装饰器的执行顺序
function Met1(): MethodDecorator {
console.log('Met1执行');
return (target: any, method: string | symbol, descriptor: any) => {
console.log('Met1应用');
};
}
function Met2(): MethodDecorator {
console.log('Met2执行');
return (target: any, method: string | symbol, descriptor: any) => {
console.log('Met2应用');
};
}
function Met3(): MethodDecorator {
console.log('Met3执行');
return (target: any, method: string | symbol, descriptor: any) => {
console.log('Met3应用');
};
}
class Info {
name: any;
@Met1()
@Met2()
@Met3()
getData(id: number, name: string) {}
}
/**
*
Met1执行
Met2执行
Met3执行
Met3应用
Met2应用
Met1应用
*/
可以得出结论, 相同的装饰器, 由上而下执行, 但是, 又是由下而上应用, 即执行其返回的方法
Reflect
在前面的介绍中, 我们会发现, 属性装饰器和参数装饰器似乎有点‘鸡肋’, 比如: 属性装饰器可以获取属性名, 可以获取类原型对象, 那然后呢? 我们就为了打印一下属性名? 这样做显然没有意义, 而我们又拿不到运行时的属性值, 又没办法对属性值做点什么; 这个装饰器似乎真没什么用? 显然不会, 专家们不可能提些毫无作用的提案, 这些装饰器想要发挥更大的作用, 还必须有一个帮手, 那就是反射元数据, 我们先来了解下反射(Reflect)的基本概念
基本概念
Reflect和Proxy一样, 都为操作对象提供了新的API, 但是不同于Proxy, 它的存在不是为了实现代理, 而是有其他几个目的:
- 统一API, 现在很多操作对象的API都在Object等内置对象上, 而这些都是语言内部方法, 后续将逐步转移到Reflect对象上, 例如, 原本属于Object的defineProperty, 在Reflect上也有, 且功能完全一致
let obj = {};
Reflect.defineProperty(obj, 'age', { value: 'jack', writable: false });
console.log(obj); // { name: 'jack' }
- 其操作更合理, 虽然Object和Reflect上都有相同的方法, 但是, Reflect提供了更加友好的错误处理方式, 前面我们设置了obj的name属性不可更改, 现在我们来更改它试试
// Object.defineProperty只能通过try {} ... catch () {}方式来处理
try {
Object.defineProperty(obj, 'age', { value: 11 });
} catch (e: any) {
console.log(e.message); // Cannot redefine property: age
}
// Reflect.defineProperty则会返回一个boolean类型来判断是否设置成功
const isOk = Reflect.defineProperty(obj, 'age', { value: '222' });
if (!isOk) {
console.log('执行错误');
}
可以看到Reflect.defineProperty返回的是一个boolean类型, 即是否成功, 这样, 更加方便我们处理逻辑;
- 将原来Object中的命令式操作转为函数式操作, 例如, 之前判断一个对象中是否有某个属性, 会使用in; 而现在可以直接使用Reflect.has来判断
let obj = { name: 'jack' };
if ('name' in obj) {
console.log('有名字');
}
const isHas = Reflect.has(obj, 'name');
console.log('isHasName:', isHas); // isHasName: true
- 可以很好地配合Proxy来实现一些操作, 因为Reflect上的方法和Proxy上的一一对应, 这样, 我们就可以通过Reflect来实现一个方法备份, 然后在此基础之上, 为原方法增加新的操作;
let obj = { name: '' };
const instance = new Proxy(obj, {
set(target, name, value, receiver) {
const isOk = Reflect.set(target, name, value, receiver);
if (isOk) {
console.log('设置成功!');
}
return isOk;
},
});
instance.name = 'jack';
在上面的案例中, 我们使用了new Proxy来监听了对象obj的设置行为, 并在Proxy的set中, 使用了Reflect.set来执行原有的设置逻辑, 而在此基础上, 增加了设置成功的日志打印功能;
反射元数据
介绍反射元数据之前, 我们要先了解什么是元数据? 其实所谓的元数据, 就是描述数据的数据, 什么叫描述数据的数据? 为什么要描述数据? 想想刚才说的属性装饰器, 我们目前只能获取属性名和原型对象, 但是我们不知道属性值, 更没办法对属性值进行限制; 那如果我们能在装饰器里'描述'一下这个属性值, 告诉程序, 这个属性值, 在运行的时候, 应该是xxx样子的, 然后程序根据你的描述, 在运行时按照你描述的样子去对比属性值, 并作出对应的操作, 那我们的装饰器是不是瞬间强大了不少, 不再是只会console的无聊玩具了! 好了说完了元数据, 我们回头来理解下, 所谓的反射元数据, 其实就是Reflect上, 专门用于处理元数据的API! 但是很遗憾, 目前为止, 元数据提案还尚未通过, 尽管它提出的很早, 所以, 这里我们就需要使用'reflect-metadata'这个npm包, 先来看看元数据的基本操作
import 'reflect-metadata';
class Foo {
info: object | undefined;
}
Reflect.defineMetadata('class:key', 'class:value', Foo);
console.log(Reflect.getMetadata('class:key', Foo)); // class:value
Reflect.defineMetadata('info:key', 'info:value', Foo, 'info');
console.log(Reflect.getMetadata('info:key', Foo, 'info')); // info:value
defineMetadata方法接收四个参数: 元数据key、元数据的值、目标对象/类、目标对象/类的属性(可选)
getMetadata方法和defineMetadata相比, 只是缺少了元数据的值这个入參, 毕竟这个方法就是拿来读取原数据的值的;
装饰器应用
前面说了, 元数据正是为了增强装饰器能力而生的, 所以, reflect-metadata也提供了反射元数据的装饰器调用方式
import 'reflect-metadata';
@Reflect.metadata('class-meta-key', 'class-meta-value')
class Foo {
@Reflect.metadata('prop-meta-key', 'prop-meta-value')
info: object | undefined;
@Reflect.metadata('method-meta-key', 'method-meta-value')
getInfo() {
return this.info;
}
}
const foo = new Foo();
console.log(Reflect.getMetadata('class-meta-key', Foo)); // class-meta-value
console.log(Reflect.getMetadata('prop-meta-key', foo, 'info')); // prop-meta-value
console.log(Reflect.getMetadata('method-meta-key', Foo.prototype, 'getInfo')); // method-meta-value
从以上案例可以看出, metadata其实就是defineMetadata, 只不过是以装饰器的方式调用罢了; 我们仍然可以通过getMetadata来获取到其不同位置的元数据; 注意了, 元数据设置的位置取决于额装饰器@Reflect.metadata调用的位置, 如果调用在class之上, 那其元数据就设置在类上; 如果在非静态属性上调用, 那自然就设置在原型对象上, 或者你说实例上也没错; 正如prop-meta-key和method-meta-key所在位置一样
添加参数
前面的装饰器中的元数据入參, 都是我们写死的, 这显然也不符合实际需要, 所以, 我们要让元数据装饰器可以接收到参数, 这样才能灵活处理各种场景, 先以最常见的校验为例, 加入我们要在运行时限定类及类成员的类型, 注意了, 是运行时, 不是开发阶段, 在运行时, 所有类型标注都不存在了!
const expectedType = Symbol('expectedType');
function expectType(Type: 'string' | 'number' | 'boolean'): PropertyDecorator {
return (target, prop) => {
Reflect.defineMetadata(expectedType, Type, target, prop);
};
}
class Student {
@expectType('string')
name: any;
@expectType('number')
age: any;
constructor(name: any, age: any) {
this.name = name;
this.age = age;
}
}
function validateType(target: any) {
const allKeys = Reflect.ownKeys(target);
for (let key of allKeys) {
const actualType = typeof target[key];
const expectType = Reflect.getMetadata(expectedType, target, key);
if (actualType !== expectType) {
console.log('类型错误');
}
}
}
validateType(new Student('小明', '18岁')); // 类型错误
我们在装饰器中使用元数据定义属性age为number类型, 再在validateType方法中进行了类型校验, 从而实现了在运行时也能校验类型的功能!
除此之外, 还能对类的属性进行必填校验:
import 'reflect-metadata';
const requiredKey = Symbol('requiredKey');
function required(): PropertyDecorator {
return (target, prop) => {
const requiredKeys = Reflect.getMetadata(requiredKey, target) ?? [];
Reflect.defineMetadata(requiredKey, [...requiredKeys, prop], target);
};
}
class Person {
@required()
name: any;
@required()
age: any;
constructor(name?: string, age?: number) {
this.name = name;
this.age = age;
}
}
let person = new Person('jack');
function validate(target: any) {
// 获取目标对象的所有属性
const allKeys = Reflect.ownKeys(target);
const requiredKeys = Reflect.getMetadata(requiredKey, target);
for (let key of requiredKeys) {
if (!target[key]) {
console.log('必填项不得为空');
}
}
}
validate(person);
本案例中, name和age都是必填, validate中获取到元数据, 然后进行对比, 从而实现必填项的校验