decorator允许我们向TypeScript中的类或方法添加额外的信息,类似于Java中的注释。类装饰器应用于TypeScript中的类定义,可以观察、修改或替换类定义。
本文深入探讨了TypeScript类装饰器的定义和使用。要使用decorators,必须在TypeScript中启用它们,所以一定要查看本系列中的decorators简介文章。类装饰器主要用于向类添加元数据,其他装饰器将使用元数据。但是,类装饰器也可以返回一个新的构造函数来覆盖或替换现有的构造函数或类,从而添加新方法或其他行为。
也就是说,在class关键字前面立即加上一个或多个decorator。通过一些装饰器,参数可以配置其行为。其他装饰师不需要参数,这将在文档中解释。
让';让我们尝试一个简单的类装饰器示例,它只打印给定的数据。
函数(构造函数:函数){const ret={constructor,:(构造函数),:(构造函数),:(构造函数),:(构造函数),:(构造函数),:{};for(const key of.(constructor.prototype)){ret.members[key]=constructor.prototype[key];}安慰('classdecoreExample',ret);]类{(x:number,y:number){console(`classdecoreExample(${x},${y})`;}(){console.(`method called`);}新的(3,4)。()
logConstructor是@logConstructor decorator的decorator函数,它为类decorators实现指定的方法签名。此装饰器不使用任何参数,并且由于其实现,不需要括号。
它只是打印出有关构造函数的可用信息。对于大多数情况,我们使用对象类中的方法来查询有关被修饰类的数据。在一些情况下,查询是针对构造函数的。原型,因为该对象包含附加到类的方法的实现细节。
存储库中的decorator-inspector包包含一个更全面的decorator LogClassInspector,具有类似的用途。
$npx ts node lib/classes/first。ts ClassDecoratorExample{constructor:[class ClassDecoratorExample],可扩展:false,冻结:false,密封:false,值:[],属性:{length:{value:2,可写:false,可枚举:false,可配置:false},名称:{value:';ClassDecoratorExample';,可写:false,可枚举:false,可配置:false},原型:{value:{},可写:false,可枚举:false,可配置:false},成员:{constructor:[classdecorexample],method:[Function:method]}classdecorexample(3,4)调用的方法
属性中的值是PropertyDescriptor对象,我们';我会再三看的。我们大多数人不需要在JavaScript的幕后深入研究这一点。但是,要实现decorators,我们需要知道一些事情。
例如,您知道对象上的密封或冻结设置吗?现在我了解了他们,它';It’启用这两种设置看起来都很有用。这个装饰师让它变得简单:
我们可以轻松地为任何可装饰的东西添加多个装饰器。多个装饰器有一个执行顺序,它们从下往上执行。因此,输出现在将显示sealed:true,因为@sealed将首先执行。如果你把@sealed放在logConstructor上面,它仍然会说sealed:false,因为@sealed将在第二个执行。
装饰师也可以接受参数。正如我们在介绍中所说,这需要遵循一种不同的模式,称为装饰工厂。
下面是一个简单的类装饰器示例,它不仅展示了如何传入参数,还帮助我们理解使用多个装饰器时的执行顺序。
函数(路径:字符串){console(`outer with param${path}`);返回(目标:函数)=>;{console.(`internal withParam${path}`);};}(';第一';)(';中';)(';最后';)类{}
外部函数withParam获取要与装饰器一起使用的参数列表。内部函数是decorator函数,是实现所需签名的地方。它是包含实际装饰器实现的内部函数。
withParam(parameter)是一个表达式,它返回一个具有正确签名的函数作为类装饰器。这使得内部函数成为装饰函数,而外部函数是生成该函数的工厂。
在这个例子中,我们';我已经三次附上param,这样我们可以了解更多关于执行顺序的信息。
$npx ts-节点库/类/构造函数。ts outer with param first outer with param middle outer with param last inner with param last inner with param middle inner with param first
我们在引言中详细讨论了这一点。请记住,工厂函数从上到下执行,之后装饰函数从下到上执行。
让';让我们来研究一下类装饰器的一个可能的实际用途。也就是说,一个框架可能会保存某些类型的类的列表。对于这个例子,我们';我们将模拟一个web应用程序框架,其中某些类具有URL路由功能。每个路由器类处理特定URL前缀的路由,以及该路由的特定配置。
常量注册类=[];函数(路径:字符串,选项?:对象){return(构造函数:函数)=>;{registeredClasses.({constructor,path,options});};}( '/') 类{//routing functions}(';/blog';,{:';/blog/rss.xml';})类{//路由函数}控制台。(注册类别);
Router是一个工厂函数,它生成一个类装饰器,将类添加到RegisteredClass数组中。该函数有两个选项,其中path是URL路径前缀,options是可选的configuraton对象。
因为班级装饰师排在最后,所以';对于类中包含的任何方法或属性,类装饰器可以选择做些什么。此外,更常见的存储数据的方法不是这样的数组,而是使用反射元数据API,我们将在后面讨论。
$npx ts节点库/类/寄存器。ts[{constructor:[class HomePageRouter],路径:';/';,选项:未定义,{constructor:[class BlogRouter],路径:';/blog';,选项:{rss:';/blog/rss.xml';}]
这取决于假设的框架来处理这些数据。正如我们前面看到的,从构造函数对象开始,有很多额外的数据可用。
这个简单的例子很有趣,但让我们来看看';让我们尝试做一些有趣的事情。它';s承诺我们可以修改或替换类定义。这需要一些魔法,所以让';让我们看看。
在开始之前,请后退半步,考虑一下这个问题——类装饰器函数接收类对象。它不接收正在创建的实例。我们可以使用Object添加属性。定义属性(目标,…),但该属性被添加到类中,而不是添加到生成的任何实例中。
为了抓住一个随机的概念,考虑一个应用程序记录事件在员工的一天。员工先打卡上班,下班后再打卡下班。让';s创建两个类来记录这些事件,并使用装饰器自动添加时间戳和唯一标识符。
从'导入{v4 as uuidv4};uuid';;功能<;T扩展{(…:any[]):{}>;(:T){return class target{uuid=();created=new()。(";en-US";);(msg:string){console.(`Extended${msg}`);}}类{//方法和属性}类{//方法和属性}
时间戳装饰器有一个奇怪的声明,但这确实有效,它来自正式的TypeScript文档,并且与类装饰器函数所需的签名匹配。extends子句似乎与扩展泛型类有关,因此在本例中,T匹配任何类定义。
带有<;T扩展{new(…args:any[]):{}}>;是泛型,其中T被定义为扩展任何类的东西。这种语法直接来自TypeScript文档,可能是声明类装饰器的更精确的方法。
返回类extends target的部分是一个类操作,它将创建一个新类来扩展被修饰的类。对于这个新班级,我们';我们添加了两个字段和一个函数。uuid字段是一个唯一标识符,创建的字段是一个时间戳。
我们';重新生成ClockIn的两个实例,以验证每个实例是否获得唯一标识符。然后我们打印出一些数据。
$npx ts节点库/类/时间戳。ts时钟{uuid:';bc3e6f35-c85d-491d-82c0-c906deacc774';,创建:';2022年2月10日,下午6:11:53';}时钟{uuid:';13eb2df6-b9e7-4735-9130-a7ee13182d33';,创建:';2022年2月10日,下午6:11:53';}时钟输出{uuid:';31ce8e88-90e0-40d7-9eb2-cbba5c879b5d';,创建:';2022年2月10日,下午6:11:53';}真正的bc3e6f35-c85d-491d-82c0-c906deacc774bc3e6f35-c85d-491d-82c0-c906deacc774Extended WorldExtended World#2
我们的对象确实有添加的字段,两个时钟实例有不同的标识符。
代码变得有趣的地方是试图直接访问一个字段,比如ci。乌伊德。编译器给出了一个错误,表示类型上不存在命名字段,可能是因为ClockIn类没有名为uuid的字段。但是我们通过调用hasOwnProperty了解到添加的属性确实存在,并且我们可以使用ci[';uuid';]访问这些属性或者通过在(<;any>;ci)中对任何ci进行强制转换。乌伊德。
虽然我们使用装饰器成功地修改了这个类,但结果有点棘手。访问添加的属性是不自然的,在本例中,这使其使用起来没有吸引力。
TypeScript文档演示了同样的问题。他们的示例显示通过相同的机制添加一个字段reportingURL。但是试图访问该属性会抛出一个错误,property';第39号报告;类型'上不存在;BugReport';。它';s解释说,因为TypeScript类型没有更改,所以类型系统不知道添加的属性。正如我们';我已经演示过,这些属性就在那里,而且这些属性可以通过跳环获得。
但是,如果我们重写类中定义的字段,会发生什么呢。考虑一下:
功能<;T扩展{(…:any[]):{}>;(:T){return class target{(w:number,h:number){return{w,h,:w*h};}}类{(w:number,h:number){返回w*h;}安慰(新的()。( 5, 6)); 安慰(新的()。( 6, 7));
我们有一个名为Override的类和一个名为Override的装饰器。该类有一个名为area的函数,它只返回明显的值,即宽度和高度相乘的结果。被重写的版本返回一个匿名对象,其中包含。
显然,这确实成功地用新功能取代了原来的功能。此外,我们不需要跳过任何障碍就可以访问最重要的功能。
如果在类装饰器中重写匿名子类中的类构造函数,会发生什么?
例如,npm/Thread存储库中有许多decorator库专注于记录对象创建、方法调用和属性访问。要了解这一点,让';让我们来看一种在创建类实例时打印日志消息的方法。
从'导入*作为util;util';;功能<;T扩展{(…:any[]):{}>;(:T){return class target{(…args:any[]){super(…args);console(`Create${util.inspect(target)}with args=`,args);}}类{this.height=height;this.width=width;}(){返回this.width*this.height;}类{:数;(直径:数){this.diameter=diameter;}(){return((this.diameter/2)**2)*(.PI);}const rect1=new(3,5);安慰(`area rect1${rect1.area()}`);const rect2=新的(5,8);安慰(`area rect2${rect2.area()}`);const rect3=new(8,13);安慰(`area rect3${rect3.area()}`);const circ1=新的(20);安慰(`area circ1${circ1.area()}`);
我们有两个简单的类,矩形和圆形。LogClassCreate装饰器扩展该类,为其提供一个自定义构造函数方法。因为这个方法使用。。。any它可以与具有任意数量参数的任何构造函数一起使用。
当程序启动时调用类装饰器时,在创建类实例时执行匿名子类的构造函数。
$npx ts node lib/classes/override2。ts Create[class]with[3,5]area rect1 15 Create[class]with[5,8]area rect2 40 Create[class]with[8,13]area rect3 104 Create[class]with[20]area circ1 314.1592653589793
以Create开头的消息来自匿名子类中的构造函数内部。实际上,这是在实例化类实例时执行的装饰器代码。
请注意,它正确地将参数记录到构造函数中,并且每个对象都已正确初始化,并生成正确的结果。最后一个在JavaScript数学的范围内是正确的,因为数学。PI是一个粗略的估计。
错误TS1329:';装饰师';这里接受的参数太少,无法用作装饰器。你是不是打算先打电话写'@Decorator()';?
本例中的问题是,如上所述,类装饰器需要接受一个参数,即类的构造函数。我们省略了那个必需的参数,错误消息可能会告诉我们这一点。
我们已经了解了很多关于课堂装饰师的知识。装饰器接收类对象,我们可以从中访问大量数据。
decorator函数在创建类对象时执行,而不是在构造类实例时执行。这意味着要直接影响生成的实例,我们必须创建一个匿名子类。
使用匿名子类可能很棘手。访问任何添加的方法或属性都需要跳转,被重写的方法或属性将透明地执行。