TypeScript装饰器
Posted DieHunter1024
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TypeScript装饰器相关的知识,希望对你有一定的参考价值。
目录
前言
本文收录于TypeScript知识总结系列文章,欢迎指正!
程序遵循开放封闭原则,即在设计和编写软件时应该尽量避免对原有代码进行修改,而是通过添加新的代码来扩展软件的功能。
在日常开发中不知你有没有遇到以下情况,我们封装了一个Request模块,现在需要对请求进行拦截,访问请求参数,此时我们可以通过装饰器针对请求函数或者请求类进行访问,获取参数并解析
定义
在TS中,装饰器是一种特殊类型的声明。可以附加到类、方法、属性或参数上用于修改类的行为或属性。
在面向对象编程中,有时需要对类的行为和功能做出修改,直接修改类的内部可能会使成本升高,或出现其他问题;此时可以使用装饰器来修改类,在保证类内部结构与功能不变的前提下对数据或行为进行迭代
TS中装饰器可以分为类装饰器、方法装饰器、属性装饰器和参数装饰器。
tips:使用装饰器前需要在tsconfig中开启experimentalDecorators属性
类装饰器
类装饰器是应用于类的构造函数的函数,它可以用来修改类的行为。类装饰器可以有一个参数,即类的构造函数,通过这个参数我们可以对类的行为进行修改。
基本用法
类装饰器的语法是在一个普通的函数名前面加上@符号,后面紧跟着要装饰的类的声明,如:
const nameDecorator = (constructor: typeof Animal) =>
console.log(constructor.prototype.name)// undefined
@nameDecorator
class Animal
name: string = "阿黄"
constructor()
console.log(this.name);// 阿黄
new Animal()
在上述代码中,我使用decorator获取Animal类的name属性,发现获取的是未定义,而在构造函数中却可以获取,原因是类的装饰器是在类定义时对类进行操作的,而属性及函数的初始化是当类实例化时进行的,所以获取不到name的值
操作方式
通过类装饰器操作类的方式有两种:操作类的原型和类的继承
操作类的原型
ES6之前的类是通过构造函数实现的,其原型prototype属性是存在的,所以我们在对类进行操作时可以使用修改原型的方式
type IAnimal =
name?: string
getName?: () => string
const nameDecorator = (constructor: Function) =>
const _this = constructor.prototype // 模拟类内部环境
_this.name = "阿黄"
_this.getName = () =>
return _this.name
@nameDecorator
class Animal implements IAnimal
const animal: IAnimal = new Animal()
console.log(animal.getName()) // 阿黄
上面代码实现了对类中name属性初始化以及实现了类的getName方法,在ES5中如何实现类的重写?看看下面代码对装饰器的修改:
const nameDecorator = (constructor: IAnimalProto) =>
const _this = constructor.prototype // 模拟类内部环境
_this.name = "阿黄"
return class extends constructor
getName = () =>
return "名字:" + _this.name
tips:如果通过这种方式无法修改类的属性或方法,可以把tsconfig中target属性调整为ES5,兼容低版本浏览器,此时类是通过构造函数实现的
类继承操作
ES6中的类语法糖中没有prototype属性,所以我们可以使用继承的方式实现上面的代码,并使用重写的方式修改类中的同名函数
type IAnimal =
name?: string
getName?: () => string
type IAnimalProto =
new(): Animal
& IAnimal
const nameDecorator = (constructor: IAnimalProto) =>
return class extends constructor
constructor(public name = "阿黄")
super()
getName() // 重写类中的函数
return "姓名:" + this.name
@nameDecorator
class Animal implements IAnimal
name?: string;
getName()
return this.name
const animal: IAnimal = new Animal()
console.log(animal.getName()) // 姓名:阿黄
方法装饰器
方法装饰器是应用于类方法的函数,它可以用来修改方法的行为。方法装饰器可以接收三个参数,分别是目标类的原型对象(若装饰的是静态方法,则指的是类本身)、方法名称和方法描述符。
需要注意的是构造函数不是类的方法,所以方法装饰器不能直接用于装饰构造函数
tips:在类中使用方法装饰器需要避免箭头函数的出现,因为箭头函数的this指向它定义的环境,而不是实例对象,这导致了它无法获取到类的属性和方法
下面是一个案例
const nameDecorator = (target: Animal, key: string, descriptor: PropertyDescriptor): PropertyDescriptor =>
console.log(target); // setName: [Function (anonymous)]
console.log(key); // setName
console.log(descriptor);
//
// value: [Function (anonymous)],
// writable: true,
// enumerable: true,
// configurable: true
//
return descriptor
class Animal
name: string
@nameDecorator
setName(name: string)
this.name = name
const animal = new Animal()
animal.setName("阿黄")
console.log(animal.name); // 阿黄
其中target表示当前函数所在的类,key一般指函数名,descriptor指当前函数对象的描述符
基于上面的代码我们可以重写一下类中的setName函数:
const nameDecorator = (target: Animal, _: string, descriptor: PropertyDescriptor): PropertyDescriptor =>
descriptor.value = (name: string) =>
target.name = "名字:" + name
return descriptor
属性装饰器
属性装饰器应用于类属性的函数,它可以用来修改属性的行为或拦截属性的定义和描述符的访问,但是不能修改属性值。属性装饰器可以接收两个参数,分别是目标类的原型对象(若装饰的是静态属性,则指的是类本身)和属性名称。
与方法装饰器不同属性装饰器不会返回descriptor这个参数,也就是无法获取到属性的描述
const nameDecorator = (target: Animal, key: string) =>
target[key] = '阿黄'
class Animal
@nameDecorator
name: string
setName(name: string)
this.name = name
const animal = new Animal()
console.log(animal.name);// 阿黄
animal.setName("小黑")
console.log(animal.name);// 小黑
tips:为什么无法使用属性装饰器给属性定义初始值? 此时可以检查一下你的tsconfig.json里面的配置target是不是设置成ES2021以后(如:ESNext,ES2022,ES2023等),在ES2022前在TS中声明了属性成员会在JS编译成第一张图,ES2022及以后显示的是第二张图
解决方法:参考这个
我们可以将属性声明修改为环境声明(declare,在后续文章会说到),或者使用旧版本的target配置
class Animal
@nameDecorator
declare name: string
setName(name: string)
this.name = name
存取器装饰器
存取器装饰器是一种特殊类型的装饰器,它可以被用来装饰类中的存取器属性。它的使用方式与方法装饰器相同。但是与方法装饰器不同的是存取器descriptor参数中没有value值,但是我们可以通过修改descriptor来重写getter和setter方法
const nameDecorator = (_: any, __: string, descriptor: PropertyDescriptor) =>
const __getter = descriptor.get;
descriptor.get = function () // 必须使用function,使用箭头函数获取不到this
const value = __getter?.call(this);// 运行get获取存取器属性
return "名字:" + value;
;
class Animal
constructor(private _name: string)
@nameDecorator
get name()
return this._name
const animal = new Animal("阿黄")
console.log(animal.name);
参数装饰器
参数装饰器是应用于类构造函数或方法参数的函数,它可以用来获取参数位置。参数装饰器可以接收三个参数,分别是目标类的原型对象(若装饰的是静态方法,则指的是类本身)、方法名称和参数索引(第几个参数,从0开始)。
基本用法
参数装饰器是一个函数,它可以被应用到类的构造函数、方法的参数上,它无法应用在访问器(getter 和 setter)的参数。
const nameDecorator = (target: any, key: string, parameterIndex: number) =>
const name = target.name ?? target.constructor.name
console.log(`$name中的$key ?? '构造函数'第$parameterIndex个参数`);
class Animal
constructor(public _name: string)
setName(@nameDecorator name: string)
this._name = name
new Animal("阿黄")
参数装饰器虽然无法直接获取或者修改参数,但是可以将参数的位置标识出来,与元数据(reflect-metadata库),以及方法装饰器配合达到过滤参数的目的
参数过滤器
下面我们借助一个简单的反射元数据操作实现一个参数过滤器
元数据函数实现
type IKey = string | symbol | number
const getReflect = () => Reflect ?? Object
const __Reflect = getReflect()
const defineMeta = (target: any, key: IKey, metadataKey: IKey, descriptor: PropertyDescriptor): void =>
__Reflect.defineProperty(target[key], metadataKey, descriptor)
const getMeta = (target: any, key: IKey, metadataKey: IKey,): PropertyDescriptor =>
return __Reflect.getOwnPropertyDescriptor(target[key], metadataKey)
参数过滤
下面我们实现一个参数过滤,如果name等于阿黄,则中断函数执行并跳出
// 存储参数的索引
const saveMeta2Arr = (target: any, key: string, parameterIndex: number, keyWord: string) =>
const paramsList = getMeta(target, key, keyWord)?.value ?? []
paramsList.push(parameterIndex)
defineMeta(target, key, keyWord, value: paramsList )
return paramsList
// 参数装饰器
const paramsDecorator = (target: any, key: string, parameterIndex: number) =>
const paramsList: string[] = saveMeta2Arr(target, key, parameterIndex, 'list:params')// 参数列表
defineMeta(target, key, 'filter:params',
value: (...args) =>
if (!!!args.length) return void 0 // 没传参数默认跳过参数校验
return paramsList.filter(it => args[it] === "阿黄").length > 0// 我的校验规则是参数等于阿黄就跳出函数,这个可以自行修改
)
// 函数装饰器
const methodDecorator = (target: Animal, key: string, descriptor: PropertyDescriptor) =>
const fn = getMeta(target, key, 'filter:params').value // 获取参数装饰器的回调函数
const method = descriptor.value
descriptor.value = function (...args)
if (fn(...args)) return console.error("跳出了函数");// 过滤操作
method.apply(this, args)
class Animal
constructor(public name?: string)
@methodDecorator
setInfo(@paramsDecorator name?: string)
console.log("执行了函数");
this.name = name
效果实践
实现完成我们实例化一下类试试,可以看到,此时由于我们传入的参数是阿黄,所以setName函数未执行
const animal = new Animal()
animal.setInfo("阿黄")
console.log(animal.name);
// 跳出了函数
// undefined
下面我们修改一下,传入一个参数:小黑
const animal = new Animal()
animal.setInfo("小黑")
console.log(animal.name);
// 执行了函数
// 小黑
上述代码执行了setName并且将name赋值了小黑
装饰器优先级
下面说说当多个装饰器作用于同一个目标时,它们执行的顺序和影响的优先级是怎样的
相同装饰器
同一种装饰器的执行顺序是从下往上,理解为就近原则,近的先执行
const decorator1 = (...args: any[]) =>
console.log(1)
const decorator2 = (...args: any[]) =>
console.log(2)
const decorator3 = (...args: any[]) =>
console.log(3)
@decorator1
@decorator2
@decorator3
class Animal
new Animal()
// 输出3 2 1
不同装饰器
不同装饰器遵循:参数>函数=属性=存取器>类,参数优先级最高,类最后执行。何以见得?
const decorator1 = (...args: any[]) =>
console.log(1)
const decorator2 = (...args: any[]) =>
console.log(2)
const decorator3 = (...args: any[]) =>
console.log(3)
const decorator4 = (...args: any[]) =>
console.log(4)
const decorator5 = (...args: any[]) =>
console.log(5)
@decorator1
class Animal
@decorator2
_name: string
@decorator3
get name()
return this._name
@decorator4
setName(@decorator5 name: string)
this._name = name
new Animal()
上面的代码输出2 3 5 4 1;我们换个顺序,把属性,函数,存取器调换位置再试试
@decorator1
class Animal
@decorator4
setName(@decorator5 name: string)
this._name = name
@decorator3
get name()
return this._name
@decorator2
_name: string
输出5 4 3 2 1。可见属性,函数,存取器优先级在同级,参数更高,类更低
装饰器工厂
使用类装饰器和方法装饰器时,我们难免会遇到参数传递的问题,每个装饰器都有可以复用的可能,为了使代码高可用,我们可以尝试使用高阶函数实现一个工厂,外部函数接收参数,函数返回装饰器
type IAnimal =
name?: string
type IAnimalProto =
new(name: string): Animal
& IAnimal
const nameDecorator = (name: string) => (constructor: IAnimalProto) =>
return class extends constructor
constructor()
super(name)
@nameDecorator("阿黄")
class Animal implements IAnimal
constructor(public name?: string)
const animal: IAnimal = new Animal()
console.log(animal.name);
上述代码中我们实现了使用装饰器工厂给Animal类的属性name赋予默认值的功能
hooks与class兼容
在实际开发中,如果是使用react开发应该会遇到这样的问题,使用类开发的组件装饰器写法是这样
@EnhanceConnect((state: any) => (
global: state['@global'].data
))
export class MyComponent
constructor(props: IProps)
// ...
如何转换成hooks写法?
export const MyComponent = EnhanceConnect((state: any) => (
global: state['@global'].data
))((props: IProps) =>
useEffect(() =>
// ...
)
)
结语
本文详细讲述了类装饰器,方法装饰器,属性装饰器,存取器装饰器,参数装饰器这五种装饰器的基本用法及注意事项;此外还针对装饰器优先级进行了排序及证明;最后介绍了装饰器工厂即装饰器的实际应用场景兼容方法。
感谢你的阅读,希望文章能对你有帮助,有任何问题欢迎留言私信,请别忘了给作者点个赞哦,感谢~
相关文章
08_TypeScript装饰器
装饰器:装饰器就是一个方法,可以注入到类、方法、属性参数上来扩展类、属性、方法、参数的功能。
写法:普通装饰器(无法传参) 、 装饰器工厂(可传参)。
1、类装饰器:类装饰器在类声明之前被声明(紧靠着类声明),类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 传入一个参数。
类装饰器:普通装饰器(无法传参)
//定义一个装饰器(其实就是一个方法) function decorator(target:any){ //target 就是当前类 console.log(target); //扩展属性 target.prototype.name=‘扩展属性‘; //扩展方法 target.prototype.run=function(){ console.log(‘扩展方法‘); } } //定义一个类并装饰(用符号@+装饰器名称,紧靠着类声明) @decorator class Dog{ constructor(){} eat(){} } //调用扩展后的属性及方法 var dog:any = new Dog(); dog.name; dog.run();
类装饰器:装饰器工厂(可传参)
//定义一个装饰器(其实就是一个方法) function decorator(params:string){ return function(target:any){ //target 就是当前类 console.log(target); //params 是自定义传入得参数 console.log(params); //扩展属性 target.prototype.name= params+‘扩展属性‘; //扩展方法 target.prototype.run=function(){ console.log(params+‘扩展方法‘); } } } //定义一个类并装饰(用符号@+装饰器名称,紧靠着类声明),及传参 @decorator(‘张三‘) class Dog{ constructor(){} eat(){} } //调用扩展后的属性及方法 var dog:any = new Dog(); dog.name; dog.run();
类装饰器:重载构造函数
//定义一个装饰器(其实就是一个方法) function decorator(target:any){ //target 就是当前类 console.log(target); //重载构造函数(定义一个类并继承当前类,修改原有的属性值及方法) return class extends target{ name:any = ‘小黑有钱了‘ eat(){ console.log(this.name + ‘而且变帅了‘) } } } //定义一个类并装饰(用符号@+装饰器名称,紧靠着类声明) @decorator class Dog{ public name:string | undefined; constructor(){ this.name = "小黑" } eat(){ console.log(this.name+‘吃东西‘) } } //调用扩展后的属性及方法 var dog:any = new Dog(); dog.name; dog.eat();
2、属性装饰器 :属性装饰器表达式会在运行时当作函数被调用。
//定义一个装饰器(其实就是一个方法) function decorator(params:any) { //属性装饰器传入俩个参数,原型对象和成员名字 return function(target: any, attr: any) { target[attr]=params; } } class Demo { //定义一个属性并装饰(用符号@+装饰器名称,紧靠着属性声明) @decorator(‘张三‘) public name:any |undefined; constructor(){} } const d = new Demo(); console.log(d.name);//张三
3、方法装饰器:可以用来监视,修改或者替换方法定义。
//定义一个装饰器(其实就是一个方法) function get(params:any){ console.log(params) //方法装饰器传入三个参数,原型,成员名字,成员描述 return function(target:any,methodName:any,desc:any){ //desc.value 原来的方法 var oMethod=desc.value; //覆盖原来的方法 desc.value=function(){ console.log(‘张三有钱了‘); //如果是修改原来的方法,而不是覆盖, //oMethod.apply(this); } } } class HttpClient{ public url:any |undefined; constructor(){ } //定义一个方法并装饰(用符号@+装饰器名称,紧靠着方法声明),及传参 @get(‘方法装饰器‘) getData(){ console.log(‘张三很穷‘); } } var http=new HttpClient(); http.getData();
4、方法参数装饰器:参数装饰器表达式会在运行时当作函数被调用,可以使用参数装饰器为类的原型增加一些元素数据。
//定义一个装饰器(其实就是一个方法) function PathParam(params:any) { //方法参数装饰器传入三个参数,原型,方法的名字,参数在函数参数列表中的索引 return function (target:any,methodName:any,paramsIndex:any){ //扩展属性 target.apiUrl=params; } } class Demo { constructor() { } //定义个方法并装饰(用符号@+装饰器名称,以传参的形式写入方法) getUser( @PathParam("userId") userId: string) { console.log(userId) } } var http:any = new Demo(); http.getUser(123456); console.log( http.apiUrl);
5、装饰器执行顺序
属性 > 方法 > 方法参数 > 类
如果有多个同样的装饰器,它会先执行后面的
以上是关于TypeScript装饰器的主要内容,如果未能解决你的问题,请参考以下文章