小陈这几天在学习 TypeScript,发现装饰器这东西挺有意思,形式上有点像Java中的注解(虽然不写Java,但还是见过的)。俗话说的好,“好记性不如烂笔头”,想着用文章的形式记录下来,也作为自己对装饰器的一篇学习笔记吧。
什么是装饰器
TypeScript 中的装饰器本质上就是一个函数,顾名思义,它是对类进行装饰的工具,可以根据用户自定义扩展类的功能。
我觉得网上这个说法更言简意赅:装饰器通过声明性语法增加了在定义类时扩充类及其成员的能力。
根据装饰器修饰类型的不同,可分为以下四种类型:
- 类装饰器
ClassDecorator - 方法装饰器
MethodDecorator - 属性装饰器
PropertyDecorator - 参数装饰器
ParameterDecorator
使用写法:@装饰器名称
环境搭建
// 全局安装TypeScript
npm i typescript -g
// 创建decorator.ts文件 => 初始化tsconfig.json
tsc --init
// 启动ts编译器并监听 => 随后会编译成 decorator.js 文件 => 执行decorator.js
tsc -w
node decorator.js
初次编写并调用装饰器的时候会出现编译错误,如下所示。
这个时候打开 tsconfig.json 文件,将下面两行注释取消即可。
环境搭建好之后,开启我们的装饰器篇章吧!
类装饰器⭐️
类装饰器只有一个参数,即被修饰的类的构造函数。
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
它在类被创建的时候立即执行,仅对类做修饰,而不对实例。
基本使用
// 定义一个类装饰器,参数为类的构造函数
const AddressDecorator: ClassDecorator = (target: Function) => {
console.log(target);
target.prototype.age = 18
target.prototype.getAddress = () => {
return '你心里'
}
}
@AddressDecorator
class User {
public sayHello(): void {
console.log('Hello World');
}
// public getAddress(){} *
}
// console.log(new User().getAddress()); *
console.log((new User() as any).getAddress())
console.log((<any>new User()).age);
VSCode中打印结果如下所示。
我们去控制台打印一下展开看看。
可以发现,AddressDecorator 类装饰器的参数确实是 User 构造函数,且我们在类装饰器中往该构造函数的原型链上添加了 getAddress 方法和 age 属性。
需要注意的是,若像上面这张图这样调用 getAddress 这个方法的时候会出现编译错误。
解决方法:要么使用断言,要么在构造函数上声明一下这个方法(只需声明,无需实现)
装饰器工厂
普通的装饰器没有办法传参,而使用装饰器工厂,根据不同的传入值实现不同的装饰器,因此它的返回值就是一个装饰器,适用于各种类型的装饰器。
const AddressDecoratorFactory = (address = '默认地址'): ClassDecorator => {
return (target: Function) => {
target.prototype.getAddress = () => address
}
}
@AddressDecoratorFactory('想去哪就去哪')
class Player{}
console.log((new Player() as any).getAddress()); // 想去哪就去哪
方法装饰器⭐️
方法装饰器有三个参数,分别是 target,propertyKey 和 descriptor。
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
target:一个对象。若为 静态方法,则为类的构造函数;若是 普通方法,则是类的原型对象。
propertyKey:被修饰的方法的名字。
descriptor:一个对象,被修饰方法的属性描述符。其结构如下所示。
{
value: [Function: 方法名],
writable: true, // 可写
enumerable: false, // 可枚举
configurable: true // 可配置
}
基本使用
const UpdateDecorator: MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
console.log(target,propertyKey,descriptor);
// 重写原来的函数
descriptor.value = () => {
console.log('new method!');
}
}
class User {
@UpdateDecorator
public getName(){
console.log('my name is xiaochen');
}
@UpdateDecorator
static info(){
console.log('this is a static method');
}
}
new User().getName()
User.info()
通过打印顺序,我们可以发现装饰器在函数创建的时候立即执行,直接重写了被修饰的函数,而不是等到函数被调用的时候才执行。
装饰器工厂实现自定义console.log()
前面介绍了装饰器工厂,这里我们使用方法装饰器工厂来简单实现一个自定义的 console.log(),根据用户的设置打印不同的字体颜色及字体大小。
const MessageDecoratorFactory = (content: string, fontSize = 16, color = 'black'): MethodDecorator => {
return (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
descriptor.value = () => {
console.log(`%c${content}`,`font-size:${fontSize}px;color:${color}`)
}
}
}
class User {
@MessageDecoratorFactory('登录成功', 20, 'green')
success() {}
@MessageDecoratorFactory('登录失败', 14, 'red')
fail() {}
}
new User().success()
new User().fail()
装饰器工厂模拟网络请求
const RequestDecoratorFactory = (url: string): MethodDecorator => {
return (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
// 先将原来的方法保存下来,之后再调用
let method = descriptor.value
// 模拟请求的过程与耗时
new Promise(resolve => {
setTimeout(() => {
resolve({status: 200, name: '小陈同学吗'})
}, 2000);
}).then(res => {
method(res)
})
}
}
// 定义UserInterface接口
interface UserInterface {
status: number
name: string
[props: string]: any
}
class User {
@RequestDecoratorFactory('https://www.xiaochentongxuema.com')
public getUser(user: UserInterface) {
console.log(user);
}
}
我们只需要填好网络请求的url,则可得到返回的数据。
属性装饰器⭐️
属性装饰器有两个参数,target 和 propertyKey。
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
target:一个对象。同方法装饰器,若为 静态属性,则为该类构造函数;若为 普通属性,则为该类的原型对象。
propertyKey:被修饰的属性名。
基本使用
这里用属性装饰器实现一下大小写转换。
const PropDecorator: PropertyDecorator = (target: Object, propertyKey: string | symbol) => {
// console.log(target, propertyKey);
let v: string
// 这里其实是一个闭包,当设置user.name的时候,v被赋值,但并没有被消灭;当获取user.name的时候,转换为大写
Object.defineProperty(target,propertyKey,{
get: () => v.toUpperCase(),
set: (value) => v = value
})
}
class User {
@PropDecorator
public name: string | undefined
@PropDecorator
static _age: number
}
let user = new User()
user.name = 'ABcdEFgHijk'
console.log(user.name); // ABCDEFGHIJK
参数装饰器⭐️
参数装饰器有三个参数,分别是 target,propertyKey 和 parameterIndex。
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
target:一个对象。按照该形参所在的方法类型 分为类的构造函数和类的原型对象。
propertyKey:方法名。【不是形参名】
parameterIndex:被修饰参数所在形参列表的索引。
基本使用
const ParamsDecorator: ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => {
console.log(target, propertyKey, parameterIndex);
}
class User {
info(name: string, @ParamsDecorator age: number){
console.log(age);
}
}
打印结果为
Reflect.metadata 元数据
是什么?
Reflect.metadata主要是用于设置/读取元数据的方法,而元数据值得是描述东西时用的数据。它可以帮助开发者在类或其成员上 挂载 一些元数据,(打印出来是看不到的),并且方便地进行数据的反射和获取。
至于适用场景我也不太清楚,如果有了解的朋友欢迎评论区交流一下。
怎么用?
这里我只简单介绍两个API,分别是定义元数据和获取元数据。有更多需要的同学可以戳戳上面的链接。
-
Reflect.defineMetadata(metadataKey, metadataValue, target[, propertyKey])metadataKey:元数据名称metadataValue:元数据值target:挂载元数据的对象propertyKey:挂在元数据对象中的属性【可省略】
-
Reflect.getMetadata(metadataKey,target[, propertyKey])metadataKey:元数据名称target:挂载元数据的对象propertyKey:挂在元数据对象中的属性【可省略】
除了这种写法,还有装饰器的形式。
class C {
@Reflect.metadata(metadataKey, metadataValue)
method() {
}
}
结合装饰器实现参数验证
环境准备
// 下载reflect-metadta
npm i reflect-metadata -S
// 取消tsconfig.json里面的注释(在文章开头已经做过了)
"emitDecoratorMetadata": true,
具体实现
// 导入
import "reflect-metadata"
const RequiredDecorator: ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => {
let requiredParams: number[] = []
requiredParams.push(parameterIndex)
// 定义元数据,将所需参数数组挂载到target对象中的propertyKey
Reflect.defineMetadata('metadataKey',requiredParams,target,propertyKey)
}
const ValidateDecorator: MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
const method = descriptor.value
// 这里不用箭头函数是因为箭头函数中没有arguments对象,巩固一下这个知识点。
descriptor.value = function() {
// 获取元数据,根据元数据名称
const requiredParams: number[] = Reflect.getMetadata('metadataKey',target,propertyKey) || []
requiredParams.forEach(index => {
if(index > arguments.length || arguments[index] === undefined) {
throw new Error('请传递有必要的参数')
}
// console.log(arguments);
return method.apply(this,arguments)
})
}
}
class User {
@ValidateDecorator
public all(name: string, @RequiredDecorator id: number) {
console.log(name,id);
}
}
new User().all('小陈同学吗',1998)
上述代码是正常运行的,因为所需参数 id 我们给了。若不给呢,控制台则会抛出我们自定义的错误。
装饰器的执行顺序
装饰器是可以叠加使用的,那既然如此,装饰器的执行顺序是怎样的呢,我们来写一段测试代码看看。
const C1: ClassDecorator = () => console.log('类装饰器1');
const C2: ClassDecorator = () => console.log('类装饰器2');
const C3: ClassDecorator = () => console.log('类装饰器3');
const M1: MethodDecorator = () => console.log('方法装饰器1');
const M2: MethodDecorator = () => console.log('方法装饰器2');
const Prop1: PropertyDecorator = () => console.log('属性装饰器1');
const Prop2: PropertyDecorator = () => console.log('属性装饰器2');
const Param1: ParameterDecorator = () => console.log('参数装饰器1');
const Param2: ParameterDecorator = () => console.log('参数装饰器2');
@C1
@C2 @C3
class Thanks {
@Prop1 @Prop2
public id:number | undefined
@M2
public fun() {}
@M1
public sayHello(name: string, @Param1 @Param2 age: number) {
console.log(name, age);
}
@Prop2
public gender: string | undefined
}
打印结果如下。
我们可以发现,根据代码位置的不同,打印的结果也不同,但总体方向是从上往下的。
若装饰器叠加,执行顺序是 从下往上,从右往左。
若不同类型装饰器相互作用,执行顺序具体情况具体分析,而不是像别的文章所说是一个固定的顺序。
- 若参数装饰器和方法装饰器共同作用于同一方法,则 参数 => 方法, 这个是确定的。
- 其余情况,根据代码书写顺序,从上往下(就像我们读代码的顺序,从上往下解析,不要和上面的记混了)。
- 类装饰器总是在最后执行。
结语
以上就是关于TypeScript装饰器的学习笔记,一开始觉得不多,写下来发现也花了不少时间。
如有纰漏,欢迎指出!