TypeScript(八)装饰器

这篇具有很好参考价值的文章主要介绍了TypeScript(八)装饰器。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

前言

定义

类装饰器

基本用法

操作方式

操作类的原型

类继承操作

方法装饰器

属性装饰器

存取器装饰器

参数装饰器

基本用法

参数过滤器

元数据函数实现

参数过滤

效果实践

装饰器优先级

相同装饰器

不同装饰器

装饰器工厂

hooks与class兼容

结语

相关文章


前言

本文收录于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及以后显示的是第二张图

TypeScript(八)装饰器TypeScript(八)装饰器

解决方法:参考这个

TypeScript(八)装饰器

我们可以将属性声明修改为环境声明(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(() => {
    // ...
  })
})

结语

本文详细讲述了类装饰器,方法装饰器,属性装饰器,存取器装饰器,参数装饰器这五种装饰器的基本用法及注意事项;此外还针对装饰器优先级进行了排序及证明;最后介绍了装饰器工厂即装饰器的实际应用场景兼容方法。

感谢你的阅读,希望文章能对你有帮助,有任何问题欢迎留言私信,请别忘了给作者点个赞哦,感谢~

相关文章

Decorators - TypeScript 中文手册

装饰器与混合对象 - TypeScript 精通指南文章来源地址https://www.toymoban.com/news/detail-412601.html

到了这里,关于TypeScript(八)装饰器的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Typescript:类的装饰器

    Typescript的装饰器我在学习typescript的时候并不是很清楚它的作用场景,直到使用了nest.js框架后,才明白其作用,于是又深入学习了一下,希望通过对装饰器的学习提高对nest.js的使用。 装饰器为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式 装饰器 是一种特

    2024年02月16日
    浏览(37)
  • FPGA学习实践之旅——前言及目录

    很早就有在博客中记录技术细节,分享一些自己体会的想法,拖着拖着也就到了现在。毕业至今已经半年有余,随着项目越来越深入,感觉可以慢慢进行总结工作了。趁着2024伊始,就先开个头吧,这篇博客暂时作为汇总篇,记录在这几个月以及之后从FPGA初学者到也算有一定

    2024年02月03日
    浏览(58)
  • 【TypeScript】TS进阶-装饰器(九)

    🐱个人主页: 不叫猫先生 🙋‍♂️作者简介:前端领域新星创作者、阿里云专家博主,专注于前端各领域技术,共同学习共同进步,一起加油呀! 💫系列专栏:vue3从入门到精通、TypeScript从入门到实践 📢资料领取:前端进阶资料以及文中源码可以找我免费领取 🔥社群招

    2024年02月21日
    浏览(42)
  • 面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?

    面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们? 在TypeScript中,装饰器( Decorators )是一种用于增强代码功能的特殊类型声明。装饰器提供了一种在类、方法、属性等代码元素上注释或修改的方式,使得我们可以通过装饰器来扩展、修改或监视代码的

    2024年02月15日
    浏览(62)
  • Python高级用法:装饰器用于缓存

    缓存装饰器与参数检查十分相似,不过它重点是关注那些内部状态不会影响输出的函数。每组参数都可以链接到唯一的结果。这种编程风格是函数式编程的特点,当输入值有限时可以使用。 因此,缓存装饰器可以将输出与计算它所需要的参数放在一起,并在后续的调用中直接

    2024年01月25日
    浏览(49)
  • 【c语言】详解c语言#预处理期过程 | 宏定义前言

    c语言系列专栏: c语言之路重点知识整合   创作不易,本篇文章如果帮助到了你,还请点赞支持一下♡𖥦)!!  主页专栏有更多知识,如有疑问欢迎大家指正讨论,共同进步! 给大家跳段街舞感谢支持!ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ 代码编译到执

    2024年02月01日
    浏览(52)
  • 【python高级用法】迭代器、生成器、装饰器、闭包

    可迭代对象:可以使用for循环来遍历的,可以使用isinstance()来测试。 迭代器:同时实现了__iter__()方法和__next__()方法,可以使用isinstance()方法来测试是否是迭代器对象 使用类实现迭代器 两个类实现一个迭代器 一个类实现迭代器 可迭代对象与迭代器的总结 一个具备了__iter_

    2024年02月03日
    浏览(41)
  • typescript Awaited<Type>教程用法

    ts4.5发布了Awaited,但是很多人不明白Awaited的用法。 首先看一下官方的说明:这种类型旨在模拟函数await中的操作async,或 s.then()上的方法——特别是它们递归解包Promise的方式。 首先看一个例子: 这里我们可以正确得到bb类型为 string 。 在js中和ts中,await和.then()都能递归得到

    2024年02月04日
    浏览(51)
  • TypeScript快速上手语法+结合vue3用法

            前言:             本篇内容不涉及TypeScript安装以及配置,具体安装及配置篇可以看下面目录,本篇只涉及TypeScript语法相关内容,及结合vue3的用法。不讲废话,简单直接直接开撸。 目录      TypeScript的具体安装及配置  TypeScript快速上手语法+结合vue3用法 1、

    2024年02月03日
    浏览(37)
  • 【中秋国庆不断更】OpenHarmony定义可动画属性:@AnimatableExtend装饰器

    @AnimatableExtend装饰器用于自定义可动画的属性方法,在这个属性方法中修改组件不可动画的属性。在动画执行过程时,通过逐帧回调函数修改不可动画属性值,让不可动画属性也能实现动画效果。 ​ ● 可动画属性:如果一个属性方法在animation属性前调用,改变这个属性的值

    2024年02月08日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包