TS装饰器

article/2025/8/27 20:29:05

请添加图片描述

通过本文你可以知道什么


  • 装饰器的发展历程
  • JS和TS装饰器有何不同
  • Angular中的装饰器到底是什么
  • 装饰器的定义,语法,作用
  • reflect-meta是什么及如何使用

前言


我们平常开发中或多或少的听说或使用过装饰器,也切身感受到了它带给我们的便利。但是应该很少去系统的了解过装饰器。不清楚装饰器到底擅长干什么,怎么干。
由于目前js和ts中的装饰器有很多不同,本期只聚焦于ts的装饰器进行探讨。
本文预计阅读时间——20分钟

装饰器的演变


  • 2015-3-24
    • stage 1阶段,也是目前广为使用的用法,也基本等同于TS开启了experimentalDecorators的用法。
  • 2018-09
    • 进入到stage2阶段,用法和stage1很大不同
  • 2021-12
    • 针对stage2提案进行了一次修改。
  • 2022-03
    • 正式进入stage3。去掉了metadata部分,使用方式没有发生太大变化。

冷知识:ts只会对Stage-3以上的提案提供支持,而TS引入装饰器实在2015年3月,差不多stage-1的时间段,这是因为在 NG-Conf上,angular团队宣布与TS团队进行合作。

JS装饰器和TS装饰器


js原生目前不支持装饰器,装饰器提案在stage-3阶段,只能通过babel体验装饰器这个新特性。TS目前实现的装饰器是基于JS装饰器stage-1的语法,所以在JS装饰器正式发布后,会和TS装饰器语法产生差异,之后看TS团队如何处理了,但预计也不是近期的事情了。

定义


装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。装饰器使用@expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息作为参数传入。
定义来自:https://www.tslang.cn/docs/handbook/decorators.html

配置


由于装饰器目前还是实验中的特定,在js中处于stage-3阶段。在ts中已经作为一项实验性予以支持。开启装饰器需要在tsconfig.json文件中启用 experimentalDecorators 编译器选项。

装饰器于2022年三月底刚进入了stage-3阶段,详情见https://github.com/tc39/proposal-decorators/pull/454

请添加图片描述

Angular中的装饰器


我们在使用angular中经常会看到此类代码
image.png

每个指令,组件,module都会有对应的@expression进行标注,完全吻合装饰器的写法。但其实这种@Component类似的写法不能称作装饰器,更贴切的叫法为注解(Annotation)。它们是用于给编译器做数据描述,最终在build阶段会完全被抹去。
注解并不产生任何行为,仅仅添加附加内容。

装饰器使用


类装饰器

类装饰器是我们最常使用到的,它的通常作用是,为该类扩展功能

  1. 类装饰器有且只有一个参数,参数为类的构造函数constructor
  2. 如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明

如果你要返回一个新的构造函数,你必须注意处理好原来的原型链。 在运行时的装饰器调用逻辑中不会为你做这些。—— 官方文档

设想有这样一个场景。
目前有一个Tank类,有一个Plane类,有一个Animal类。这三个类都需要一个公共的方法来获取他们所在的位置。我们第一可能想到使用继承来实现。

class BaseClass {getPosition() {return {x: 100,y: 200,z: 300,}}
}
class Tank extends BaseClass{}
class Plane extends BaseClass {}
class Animal extends BaseClass {}

这样三个类都可以调用getPosition方法来获取各自的位置了。到目前为止看起来没什么问题。

现在又有了一个新的诉求,Tank 类和Plane类需要一个新的方法addPetrol来给坦克和飞机加油。而动物不需要加油。此时这种写法好像不能继续进行下去了。而js目前没有直接语法提供多继承的功能,我们的继承方式好像行不通了。这时候装饰器可以很完美的实现这样的功能。此时就可以请我们的装饰器闪亮登场了~


装饰器功能之——能力扩展
我们把getPositionaddPertrol都抽象成一个单独的功能,它们得作用是给宿主扩展对应的功能。

const getPositionDecorator: ClassDecorator = (constructor: Function) => {constructor.prototype.getPosition = () => {return [100, 200]}
}const addPetrolDecorator: ClassDecorator = (constructor: Function) => {constructor.prototype.addPetrol = () => {// do somethingconsole.log(`${constructor.name}进行加油`);}
}@addPetrolDecorator
@getPositionDecorator
class Tank {}
@addPetrolDecorator
@getPositionDecorator
class Plane {}@getPositionDecorator
class Animal {}

这样的话,加入日后我们有其他的猫猫狗狗,都可以对他进行能力扩展,让其具有加油的能力。

多个装饰器叠加的时候,执行顺序为离被装饰对象越近的装饰器越先执行。

装饰器功能之——重载构造函数
在类装饰器中如果返回一个值,它会使用提供的构造函数来替换类的声明。

function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {return class extends constructor {newProperty = "new property";hello = "override";}
}@classDecorator
class Greeter {property = "property";hello: string;constructor(m: string) {this.hello = m;}
}

这个一个官方的例子,暂时没有想到业务中的适用场景。

方法装饰器

方法装饰器也是非常常用的,(敲黑板)这道题去年没考,今年肯定考~
image.png
方法装饰器接受三个参数:

  1. 对于静态方法,第一个参数为类的构造函数。对于实例方法,为类的原型对象。
  2. 第二个参数为方法名。
  3. 第三个参数为方法描述符。
  4. 方法装饰器可以有返回值,返回值会作为方法的属性描述符

装饰器功能之——能力增强
我们带代码编写时候,经常会做一些错误catch。

class MusicSystem {getMusicById(name: string): Promise<{name: string, singer: string}> {return new Promise((resolve, reject) => {setTimeout(() => {if (Math.round(Math.random())) {resolve({name: '凤凰传奇', singer: '玲花|曾毅'});} else {reject()}}, 1000);})}async play(name: string) {// ... do somethingtry {const music = await this.getMusicById(name);console.log(`在曲库中找到了名为${music.name}的音乐,由${music.singer}进行演唱,敬请欣赏。`);} catch (error) {throw new Error(`未找到名为${name}的音乐,播放失败`);}}
}const musicSystem = new MusicSystem();
musicSystem.play('凤凰传奇');

有一个音乐系统,可以进行音乐播放。在播放时候,如果未找到对应的歌,会throw对应的错误。我们正常会想象到用如上方式实现。现在我们需要为音乐播放器增加一个删除歌曲的功能,并且在失败时候也需要throw出对应的异常。继续撸代码

class MusicSystem {...async deleteByName(name: string) {// ... do somethingtry {const music = await this.getMusicById(name);// ... do somethingconsole.log(`${music.name}音乐删除成功!`);} catch (error) {throw new Error(`未找到名为${name}的音乐,删除失败`);}}
}

easy,很快啊,就写出来了。但是我们发现,我们的代码结构,有很多相同的地方。作为一个程序员,是绝对不能容忍这样的事情发生!这时候,使用装饰器,也许是一种很好的解决方式。使用装饰器对每个方法进行增加,使它们自动获取catch错误的能力~

const ErrorDecorator: MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {const sourceMethod = descriptor.value;descriptor.value = async function (...args: any) {try {await sourceMethod.apply(this, args);} catch (error) {console.error('捕获到了错误');// do something}}
}
class MusicSystem {getMusicById(name: string): Promise<{name: string, singer: string}> {return new Promise((resolve, reject) => {setTimeout(() => {if (Math.round(Math.random())) {resolve({name: '凤凰传奇', singer: '玲花|曾毅'});} else {reject()}}, 1000);})}@ErrorDecoratorasync play(name: string) {const music = await this.getMusicById(name);// ... do somethingconsole.log(`在曲库中找到了名为${music.name}的音乐,由${music.singer}进行演唱,敬请欣赏。`);}@ErrorDecoratorasync deleteByName(name: string) {const music = await this.getMusicById(name);// ... do somethingconsole.log(`${music.name}音乐删除成功!`);}
}const musicSystem = new MusicSystem();
musicSystem.play('凤凰传奇');
musicSystem.deleteByName('凤凰传奇');

我们定义了一个错误捕获装饰器,名为ErrorDecorator该装饰器可以将宿主中throw出的错误捕获到。这样,我们不管以后扩展多少个功能,只要需要捕获错误,就可以使用该装饰器。业务中例如错误埋点上报等也是很适用的。

细心的同学可以发现了,我们在decorator中无法捕获到实际的错误,比如精准报错哪首歌没找到。很遗憾,目前装饰器的原生能力,是无法获取到我们调用时候传入的具体参数的。因为装饰器实在编译阶段执行的。但是,我们可以通过其他方式实现这样的功能,这就是大名鼎鼎的 metadata 。我们会在文章的末尾提到它。

装饰器功能之——descriptor修改
通过修改descriptor,我们可以实现对方法进行重新描述。比如设置方法禁止修改,禁止删除等。

const DescriptorDecorator: MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) : object => {return {value: () => {console.log('eat方法被替换')},writable: true,enumerable: true,configurable: true,};
}class Pig {name = 'peiqi';@DescriptorDecoratoreat() {}
}

同样的,也可以直接对descriptor进行修改。

descriptor.value = () => {console.log('eat方法被替换')};
descriptor.writable = true;
descriptor.enumerable = true;
descriptor.configurable = true;

方法装饰器的使用方式很多,大多数的使用方式是对descriptor的value属性进行替换,拦截等实现功能。

【下边的三个装饰器类型,相对来说使用比较少,有兴趣的小伙伴可以卷】

属性装饰器

属性装饰器接受两个参数

  1. 对于静态属性,第一个参数为类的构造函数。对于实例属性,参数为类的原型对象
  2. 第二个参数为属性名称

返回值将被忽略

网上有很多教程在使用属性装饰器时候,使用defineProperty对属性设置getter和setter,这是非常错误的用法!!官方文档已经明确说明了不能使用属性装饰器类监听和修改属性。https://www.tslang.cn/docs/handbook/decorators.html

装饰器功能之——初始化属性

const initCarPropertyDec  = <T>(property: T) => {return (target: object, propertyKey: string | symbol) => {target[propertyKey] = property;}
}class Car {@initCarPropertyDec('奔驰')name!: string;
}console.log(new Car().name)

属性装饰器还有一个更为常用的功能,配合reflect-metadata来向属性中添加元数据。并在恰当的时候消费它。
image.png
例如angular中,经常会对属性加上此类装饰器。它们就是向对应属性添加元数据。我们更贴切的把其称作为注解。

参数装饰器

参数装饰器接受三个参数

  1. 对于静态方法,第一个参数为类的构造函数。对于实例方法,为类的原型对象。
  2. 第二个参数为参数所在的方法名称。
  3. 第三个参数为参数在参数列表中的索引。

参数装饰器的返回值会被忽略。

参数装饰器一般用来做参数校验,在ts中使用场景很少

import 'reflect-metadata'const validate: MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {const method = descriptor.value;descriptor.value = function (...args: Array<any>) {const paramIndexArr = Reflect.getMetadata('required', target, propertyKey);paramIndexArr.forEach((index: number) => {if(args[index] === undefined) {throw new Error(`${index}参数未必传项!`)}})method.apply(this, args);}
}const required: ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => {const paramIndexArr = Reflect.getMetadata('required', target, propertyKey) || [];paramIndexArr.push(parameterIndex);Reflect.defineMetadata('required', paramIndexArr, target, propertyKey);
}class SSO {@validatelogin(@required username: string, @required password: string) {}
}

以上demo有一个单点登录类,其中login方法必须传入usernamepassword。我们使用参数装饰器,当函数未传入指定类型数据时候进行报错。

参数装饰器基本是用于对参数进行验证,并自定义报错信息,在ts作用较小。

访问器装饰器

接受三个参数

  • 对于静态成员,第一个参数为类的构造函数。对于实例方法,为类的原型对象。
  • 第二个参数为访问器名称
  • 第三个参数为成员的属性描述符

注意!ts不允许同时装饰一个成员的get和set访问器。一个成员的所有装饰器必须应用于文档顺序的第一个访问器上。因为装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。

该装饰器的使用方法和方法装饰器一致,因为getter,setter本质也是一对方法。

元数据和reflect-metadata

本文只对TS的装饰器进行讲解,元数据reflect-metadata简单进行普及即可。

元数据概念

元素据是用来描述数据的数据。

例如,一张照片,照片本身是数据。而元数据就是照片的大小,分辨率,拍摄地等描述这张照片的数据。

Reflect-metadata

reflect-metadata是ES7的一个提案,目前还没有实现。现在可以通过reflect-metadata这个库手动引入这个特性。
提案链接:https://rbuckton.github.io/reflect-metadata/
github: https://github.com/rbuckton/reflect-metadata#api

API声明:

namespace Reflect {// 用于装饰器function metadata(metadataKey: any, metadataValue: any): {(target: Function): void;(target: Object, propertyKey: string | symbol): void;};// 在对象或属性上面定义元数据function defineMetadata(metadataKey: any, metadataValue: any, target: Object): void;function defineMetadata(metadataKey: any, metadataValue: any, target: Object, propertyKey: string | symbol): void;// 检查对象或属性的原型链上是否存在元数据function hasMetadata(metadataKey: any, target: Object): boolean;function hasMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): boolean;// 检查对象或属性的原型链上是否存在自己的元数据function hasOwnMetadata(metadataKey: any, target: Object): boolean;function hasOwnMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): boolean;// 获取对象或属性上的元数据键的元数据值function getMetadata(metadataKey: any, target: Object): any;function getMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): any;// 获取对象或属性上自己的元数据键的元数据值function getOwnMetadata(metadataKey: any, target: Object): any;function getOwnMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): any;// 获取对象或属性原型链上的所有元数据键function getMetadataKeys(target: Object): any[];function getMetadataKeys(target: Object, propertyKey: string | symbol): any[];// 获取对象或属性的所有自己的元数据键function getOwnMetadataKeys(target: Object): any[];function getOwnMetadataKeys(target: Object, propertyKey: string | symbol): any[];// 从对象或属性中删除元数据function deleteMetadata(metadataKey: any, target: Object): boolean;function deleteMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): boolean;
}// 需要在 tsconfig.json 配置的开关:
{"experimentalDecorators": true, "emitDecoratorMetadata": true,     
} 

有好奇的小伙伴可能有一个疑问,这元数据到底是存放在哪里?会是一个普通的map么?

猜对了一半,存储也是按照常会的key value对,但是是使用weak map来存储,这样既可以保存数据,又不会影响数据源本身。

总结

  • 装饰器很擅长在不破坏原有代码结构的情况下,为其扩展功能。
  • 装饰器配合metadata可以实现很多强大的功能。

本文都是作者基于官方文档和各路大神及其自身实践整理出来的。文档中如果有错误的地方请各位指正~


http://chatgpt.dhexx.cn/article/EJhOV6dJ.shtml

相关文章

python的装饰器(函数装饰器)

作为许多语言都存在的高级语法之一&#xff0c;装饰器是你必须掌握的知识点。 装饰器&#xff08;Decorator&#xff09;&#xff1a;从字面上理解&#xff0c;就是装饰对象的器件。可以在不修改原有代码的情况下&#xff0c;为被装饰的对象增加新的功能或者附加限制条件或者帮…

python装饰器这一篇就够了

前言 什么是装饰器 实现原理 实现效果 适用场景 装饰器&#xff1a; 装饰器的写法 装饰器的拓展 装饰器的模板 什么是装饰器呢&#xff1f; 就是在特定条件下为某些函数再不改动函数体的时候为函数新添加一些功能&#xff0c;这就是装饰器 实现原理&#xff1a; …

【Python】闭包与装饰器

1.闭包 闭包的本质就是函数的嵌套定义&#xff0c;即在函数内部再定义函数 闭包有两种不同的方式&#xff0c;第一种是“返回一个函数名称”&#xff0c;第二种是在函数内部就“直接调用” 闭包的定义&#xff1a;在一个外函数中定义了一个内函数&#xff0c;内函数里运用了…

Python装饰器的通俗理解

看了这篇文章终于搞懂了装饰器&#xff0c;原文有少许错误&#xff0c;但无伤大雅&#xff0c;改正后转载于此&#xff0c;一起学习。 在学习Python的过程中&#xff0c;我相信有很多人和我一样&#xff0c;对Python的装饰器一直觉得很困惑&#xff0c;我也是困惑了好久&#x…

什么是装饰器?

一、引出装饰器概念 引入问题&#xff1a; 定义了一个函数&#xff0c;想在运行时动态的增加功能&#xff0c;又不想改动函数本身的代码&#xff1f; 示例&#xff1a; 希望对下列函数调用增加log功能&#xff0c;打印出函数调用&#xff1a; def f1(x): return x*2 d…

Python自定义装饰器

文章目录 1.闭包2.不带参数的装饰器3.带参数的装饰器4.不定长参数的装饰器5.多重装饰器修饰6.返回值是装饰器7.装饰器类 Python学习笔记—装饰器 装饰器&#xff1a;从生活角度理解&#xff0c;是对一个东西进行装饰&#xff0c;增加它本身的一些功能和内容&#xff1b; Python…

Python的装饰器

前言&#xff1a; &#x1f921; 作者简介&#xff1a;我是Morning&#xff0c;计算机的打工人&#xff0c;想要翻身做主人 &#x1f648; &#x1f648; &#x1f648; &#x1f3e0; 个人主页&#xff1a;Morning的主页 &#x1f4d5;系列专栏&#xff1a;&#…

装 饰 器

一&#xff0c;装饰器概念 装饰器本质上还是函数&#xff0c;让其它的函数在不做任何代码修改的情况下&#xff0c;增加额外的功能 所以说一句话&#xff1a;还是函数&#xff0c;记住奥 谈到一个原则&#xff1a;开发封闭原则 概念&#xff1a; 一个以函数作为参数并返回一…

学习TypeScript20(装饰器Decorator)

Decorator 装饰器是一项实验性特性&#xff0c;在未来的版本中可能会发生改变 它们不仅增加了代码的可读性&#xff0c;清晰地表达了意图&#xff0c;而且提供一种方便的手段&#xff0c;增加或修改类的功能 若要启用实验性的装饰器特性&#xff0c;你必须在命令行或tsconfig…

python装饰器详解

python中的装饰器(decorator)一般采用语法糖的形式&#xff0c;是一种语法格式。比如&#xff1a;classmethod&#xff0c;staticmethod&#xff0c;property&#xff0c;xxx.setter&#xff0c;wraps()&#xff0c;func_name等都是python中的装饰器。 装饰器&#xff0c;装饰的…

【Python】一文弄懂python装饰器(附源码例子)

目录 前言 一、什么是装饰器 二、为什么要用装饰器 三、简单的装饰器 四、装饰器的语法糖 五、装饰器传参 六、带参数的装饰器 七、类装饰器 八、带参数的类装饰器 九、装饰器的顺序 总结 写在后面 前言 最近有人问我装饰器是什么&#xff0c;我就跟他说&#xff…

偏微分方程数值解程序设计与实现——数学基础

常用算子符号 梯度算子 R d \mathbb{R}^d Rd空间中标量函数 u ( x ) u(\bf{x}) u(x)&#xff0c;其梯度算子定义如下&#xff1a; g r a d u ( x ) ∇ u ( x ) [ ∂ u ∂ x 0 ∂ u ∂ x 1 ⋮ ∂ u ∂ x d − 1 ] grad u(\mathbf{x})\nabla u(\mathbf{x}) \begin{bmatrix} …

偏微分方程的数值解(六): 偏微分方程的 pdetool 解法

偏微分方程的数值解系列博文&#xff1a; 偏微分方程的数值解(一):定解问题 & 差分解法 偏微分方程的数值解(二): 一维状态空间的偏微分方程的 MATLAB 解法 偏微分方程的数值解(三): 化工应用实例 ----------触煤反应装置内温度及转换率的分布 偏微分方程的数值解(四):…

微分方程数值解

一阶问题举例&#xff1a; 高阶问题举例 &#xff1a; 常微分方程数值解&#xff1a;向前欧拉方法之一阶问题 clc,clear,close all; a0;%初始时刻 b2*pi;%结束时刻 n100;%离散点数量 x00;%初值 h(b-a)/n;%步长 xx0 [0:n]*h;%离散点数组 funinline(sin(x)y,x,y); y01; %计算 y(…

常微分方程数值解法1

&#xff11;.牛顿迭代法 多数方程不存在求根公式&#xff0c;因此求精确根非常困难&#xff0c;甚至不可能&#xff0c;从而寻找方程的近似根就显得特别重要。牛顿迭代法使用函数 的泰勒级数的前面几项来寻找方程 的根。牛顿迭代法是求方程根的重要方法之一&#xff0c;其最大…

差分、偏微分方程的解法

这里写目录标题 微分方程数值求解——有限差分法matlab代码差分法的运用&#xff08;依旧是连续变量——>离散网格点&#xff09; PDE求解思路demo1demo2 微分方程数值求解——有限差分法 差分方法又称为有限差分方法或网格法&#xff0c;是求偏微分方程定解问题的数值解中…

微分方程数值解法(2)——椭圆型方程的有限差分法

此处参考教材为李荣华的《微分方程数值解法》 使用工具&#xff1a;Matlab 1. 算法&#xff1a;矩形网格上5点差分格式 2. 算法 I.需要求解的函数 function [v,vx,vy,f,aa,bb,cc,dd]u2D(x,y,ft)% ft为方程编号&#xff0c;u1D为精确解函数u&#xff08;t&#xff09;,注意与…

基础数学(8)——常微分方程数值解法

文章目录 期末考核方式基础知识解析解&#xff08;公式法&#xff09;解析解例题&#xff08;使用公式法&#xff0c;必考&#xff09;解析解的局限性 数值解数值解的基本流程 显示Euler法显示欧拉&#xff08;差值理解&#xff09;显示欧拉&#xff08;Taylor展开理解&#xf…

微分方程数值解法(1)——常微分方程初值问题的数值解法

此处参考教材为李荣华的《微分方程数值解法》 使用工具&#xff1a;Matlab 1. 算法 注: 最后一行应为k4,上面为笔误 2. 算法 I.需要求解的函数 function ff1D(t,u,ft)% ft为方程编号&#xff0c;u1D为精确解函数u(t),注意与f1D对应右端项函数f(t,u(t))switch ftcase 1 %P10…

偏微分方程数值解法pdf_天生一对,硬核微分方程与深度学习的联姻之路

机器之心原创 作者&#xff1a;蒋思源 微分方程真的能结合深度神经网络&#xff1f;真的能用来理解深度神经网络、推导神经网络架构、构建深度生成模型&#xff1f;本文将从鄂维南、董彬和陈天琦等研究者的工作中&#xff0c;窥探微分方程与深度学习联袂前行的路径。 近日&…