TypeScript是JavaScript语言的扩展,它使用JavaScript的运行时和编译时类型检查器。 这种组合允许开发人员使用完整的JavaScript生态系统和语言功能,同时,还可以在其之上添加可选的静态类型检查、枚举、类和接口。这些额外功能之一是装饰器的支持。 装饰器是一种装饰类成员或类本身的方法,具有额外的功能。 当我们将装饰器应用于类或类成员时,我们实际上是在调用一个函数,该函数将接收被装饰内容的详细信息,然后,装饰器实现将能够动态转换代码,添加额外的功能,并且减少样板代码。 它们是在TypeScript中进行元编程的一种方式,TypeScript是一种编程技术,使程序员能够创建使用来自应用程序本身的其他代码作为数据的代码。 本教程将分享如何在TypeScript中为类和类成员创建自己的装饰器,以及如何使用它们。 它将引导我们完成不同的代码示例,我们可以在自己的TypeScript环境或TypeScriptPlayground(一个允许我们直接在浏览器中编写TypeScript的在线环境)中遵循这些示例。 准备工作 要完成本教程实例,我们需要做如下准备:一个环境,我们可以在其中执行TypeScript程序以跟随示例。要在本地计算机上进行设置,您将需要以下内容。 为了运行处理TypeScript相关包的开发环境,同时,安装了Node和npm(或yarn)。本教程使用Node。js版本14。3。0和npm版本6。14。5进行了测试。 如果是要在macOS或Ubuntu18。04上安装,请按照如何在macOS上安装Node。js和创建本地开发环境或如何在Ubuntu18。04上安装Node。js的使用PPA安装部分中的步骤进行操作。如果您使用的是适用于Linux的Windows子系统(WSL),这也适用。此外,我们还需要在机器上安装TypeScript编译器(tsc)。为此,请参阅官方TypeScript网站。如果我们不想在本地机器上创建TypeScript环境,我们可以使用官方的TypeScriptPlayground来跟随。我们将需要足够的JavaScript知识,尤其是ES6语法,例如解构、rest运算符和导入导出。本教程将参考支持TypeScript并显示内联错误的文本编辑器的各个方面。这不是使用TypeScript所必需的,但确实可以更多地利用TypeScript功能。为了获得这些好处,我们可以使用像VisualStudioCode这样的文本编辑器,它完全支持开箱即用的TypeScript。你也可以在TypeScriptPlayground中尝试这些好处。本教程中显示的所有示例都是使用TypeScript4。2。2版创建的。 在TypeScript中启用装饰器支持 目前,装饰器在TypeScript中仍然是一个实验性功能,因此,必须先启用它。在本节中,我们将了解如何在TypeScript中启用装饰器,具体取决于您使用TypeScript的方式。 TypeScript编译器CLI 要在使用TypeScriptCompilerCLI(tsc)时启用装饰器支持,唯一需要的额外步骤是传递一个附加标志experimentalDecorators:tscexperimentalDecorators tsconfig。json 在具有tsconfig。json文件的项目中工作时,要启用实验性装饰器,我们必须将实验性装饰器属性添加到compilerOptions对象:{compilerOptions:{experimentalDecorators:true}} 在TypeScriptPlayground中,装饰器默认启用。 使用装饰器语法 在本节中,我们将在TypeScript类中应用装饰器。 在TypeScript中,我们可以使用特殊语法expression创建装饰器,其中expression是一个函数,将在运行时自动调用,其中包含有关装饰器目标的详细信息。 装饰器的目标取决于我们添加它们的位置。目前,装饰器可以添加到类的以下组件中:类声明本身特性访问器方法参数 例如,假设我们有一个名为seal的装饰器,它在类中调用Object。seal。要使用我们的装饰器,我们可以编写以下内容:sealedclassPerson{} 请注意,在突出显示的代码中,我们在密封装饰器的目标之前添加了装饰器,在本例中为Person类声明。 这同样适用于所有其他类型的装饰器:classDecoratorclassPerson{propertyDecoratorpublicname:accessorDecoratorgetfullName(){。。。}methodDecoratorprintName(parameterDecoratorprefix:string){。。。}} 要添加多个装饰器,请将它们一个接一个地添加在一起:decoratorAdecoratorBclassPerson{} 在TypeScript中创建类装饰器 在本节中,我们将完成在TypeScript中创建类装饰器的步骤。 对于名为decoratorA的装饰器,我们告诉TypeScript它应该调用函数decoratorA。将调用decoratorA函数,其中包含有关如何在代码中使用装饰器的详细信息。 例如,如果您将装饰器应用于类声明,则该函数将接收有关该类的详细信息。此功能必须在您的装饰器工作的范围内。 要创建自己的装饰器,我们必须创建一个与装饰器同名的函数。也就是说,要创建您在上一节中看到的密封类装饰器,您必须创建一个接收一组特定参数的密封函数。让我们这样做:sealedclassPerson{}functionsealed(target:Function){Object。seal(target);Object。seal(target。prototype);} 传递给装饰器的参数将取决于装饰器的使用位置。第一个参数通常称为目标。 密封装饰器将仅用于类声明,因此,我们的函数将接收一个参数,即目标,其类型为Function。这将是应用装饰器的类的构造函数。 然后,在密封函数中,在目标(即类构造函数)以及它们的原型上调用Object。seal。当这样做时,不能将新属性添加到类构造函数或其属性中,并且现有属性将被标记为不可配置。 重要的是要记住,目前在使用装饰器时无法扩展目标的TypeScript类型。这意味着,例如,你无法使用装饰器将新字段添加到类并使其成为类型安全的。 如果在密封类装饰器中返回了一个值,该值将成为该类的新构造函数。如果想完全覆盖类构造函数,这很有用。 已经创建了第一个装饰器,并将它与一个类一起使用。 接下来,我们将学习如何创建装饰器工厂。 创建装饰器工厂 有时,我们需要在应用装饰器时将其他选项传递给装饰器,为此,我们必须使用装饰器工厂。 在这里,我们将学习如何创建和使用这些工厂。 装饰器工厂是返回另一个函数的函数。他们收到这个名字是因为他们不是装饰器实现本身。 相反,它们返回另一个负责实现装饰器的函数并充当包装函数。通过允许客户端代码在使用装饰器时将选项传递给装饰器,它们在使装饰器可定制方面很有用。 假设,有一个名为decoratorA的类装饰器,并且,我们想添加一个可以在调用装饰器时设置的选项,例如,布尔标志,可以通过编写类似于以下的装饰器工厂来实现此目的:constdecoratorA(someBooleanFlag:boolean){return(target:Function){}} 在这里,decoratorA函数返回另一个带有装饰器实现的函数。注意,装饰器工厂如何接收一个布尔标志作为它的唯一参数:constdecoratorA(someBooleanFlag:boolean){return(target:Function){}} 我们可以在使用装饰器时传递此参数的值。 请参阅以下示例中突出显示的代码:constdecoratorA(someBooleanFlag:boolean){return(target:Function){}}decoratorA(true)classPerson{} 在这里,当我们使用decoratorA装饰器时,将调用装饰器工厂,并将someBooleanFlag参数设置为true。 然后,装饰器实现本身将运行。这允许我们根据使用方式更改装饰器的行为,从而,使我们的装饰器易于自定义和通过应用程序重用。 请注意,我们需要传递装饰器工厂预期的所有参数。如果,我们只是应用装饰器而不传递任何参数,如下例所示:constdecoratorA(someBooleanFlag:boolean){return(target:Function){}}decoratorAclassPerson{} TypeScript编译器会给你两个错误,这可能会因装饰器的类型而异。对于类装饰器,错误是1238和1240:Unabletoresolvesignatureofclassdecoratorwhencalledasanexpression。Type(target:Function)voidisnotassignabletotypetypeofPerson。Type(target:Function)voidprovidesnomatchforthesignaturenew():Person。(1238)ArgumentoftypetypeofPersonisnotassignabletoparameteroftypeboolean。(2345) 我们刚刚创建了一个能够接收参数并根据这些参数更改其行为的装饰器工厂。 在下一步中,我们将学习如何创建属性装饰器。 创建属性装饰器 类属性是另一个可以使用装饰器的地方,在这里,我们将了解如何创建它们。 任何属性装饰器都接收以下参数:对于静态属性,类的构造函数,对于所有其他属性,类的原型。成员的姓名。 目前,没有办法获取属性描述符作为参数。这是由于TypeScript中属性装饰器的初始化方式。 这是一个装饰器函数,它将成员的名称打印到控制台:constprintMemberName(target:any,memberName:string){console。log(memberName);};classPerson{printMemberNamename:stringJ} 当我们运行上面的TypeScript代码时,你会在控制台中看到如下打印:name 我们可以使用属性装饰器来覆盖被装饰的属性。这可以通过Object。defineProperty与属性的新setter和getter一起使用来完成。 让我们看看如何创建一个名为的装饰器allowlist,它只允许将属性设置为静态允许列表中存在的值:constallowlist〔Jon,Jane〕;constallowlistOnly(target:any,memberName:string){letcurrentValue:anytarget〔memberName〕;Object。defineProperty(target,memberName,{set:(newValue:any){if(!allowlist。includes(newValue)){}currentValuenewV},get:()currentValue});}; 首先,我们要在代码顶部创建一个静态许可名单:constallowlist〔Jon,Jane〕; 然后,我们创建一个属性装饰器:constallowlistOnly(target:any,memberName:string){letcurrentValue:anytarget〔memberName〕;Object。defineProperty(target,memberName,{set:(newValue:any){if(!allowlist。includes(newValue)){}currentValuenewV},get:()currentValue});}; 请注意,我们如何使用any作为目标的类型:constallowlistOnly(target:any,memberName:string){ 对于属性装饰器来说,目标参数的类型可以是类的构造函数,也可以是类的原型,在这种情况下使用any比较容易。 在装饰器实现的第一行中,我们将被装饰的属性的当前值存储到currentValue变量中:letcurrentValue:anytarget〔memberName〕; 对于静态属性,这将设置为其默认值(如果有)。 对于非静态属性,这将始终未定义。这是因为在运行时,在编译的JavaScript代码中,装饰器在实例属性设置为其默认值之前运行。 然后,我们将使用Object。defineProperty覆盖该属性:Object。defineProperty(target,memberName,{set:(newValue:any){if(!allowlist。includes(newValue)){}currentValuenewV},get:()currentValue}); Object。defineProperty调用有一个getter和一个setter。getter返回存储在currentValue变量中的值。 如果currentVariable在允许列表中,setter会将其值设置为newValue。 让我们使用您刚刚编写的装饰器。创建以下Person类:classPerson{allowlistOnlyname:stringJ} 我们现在将创建类的新实例,并测试设置并获取name实例属性:constallowlist〔Jon,Jane〕;constallowlistOnly(target:any,memberName:string){letcurrentValue:anytarget〔memberName〕;Object。defineProperty(target,memberName,{set:(newValue:any){if(!allowlist。includes(newValue)){}currentValuenewV},get:()currentValue});};classPerson{allowlistOnlyname:stringJ}constpersonnewPerson();console。log(person。name);person。namePconsole。log(person。name);person。nameJconsole。log(person。name); 运行代码,我们应该看到以下输出:OutputJonJonJane 该值永远不会设置为Peter,因为Peter不在允许列表中。 如果我们想让代码更具可重用性,允许在应用装饰器时设置允许列表,该怎么办?这是装饰器工厂的一个很好的用例。 让我们通过allowlistOnly装饰器变成装饰器工厂来做到这一点。constallowlistOnly(allowlist:string〔〕){return(target:any,memberName:string){letcurrentValue:anytarget〔memberName〕;Object。defineProperty(target,memberName,{set:(newValue:any){if(!allowlist。includes(newValue)){}currentValuenewV},get:()currentValue});};} 在这里,我们将之前的实现包装到另一个函数中,即装饰器工厂。装饰器工厂接收一个名为允许列表的参数,它是一个字符串数组。 现在,要使用的装饰器,我们必须通过许可名单,如以下突出显示的代码所示:classPerson{allowlistOnly(〔Claire,Oliver〕)name:stringC} 尝试运行与之前编写的代码类似的代码,但有新的更改:constallowlistOnly(allowlist:string〔〕){return(target:any,memberName:string){letcurrentValue:anytarget〔memberName〕;Object。defineProperty(target,memberName,{set:(newValue:any){if(!allowlist。includes(newValue)){}currentValuenewV},get:()currentValue});};}classPerson{allowlistOnly(〔Claire,Oliver〕)name:stringC}constpersonnewPerson();console。log(person。name);person。namePconsole。log(person。name);person。nameOconsole。log(person。name); 输出如下:OutputClaireClaireOliver 显示它按预期工作,person。name永远不会设置为Peter,因为Peter不在给定的白名单中。 现在,我们已经使用普通装饰器函数和装饰器工厂创建了第一个属性装饰器,是时候看看如何为类访问器创建装饰器了。 创建访问器装饰器 在这里,我们将了解装饰类访问器。 就像属性装饰器一样,访问器中使用的装饰器接收以下参数:对于静态属性,类的构造函数,对于所有其他属性,类的原型。成员的姓名。 但与属性装饰器不同的是,它还接收第三个参数,即访问器成员的属性描述符。 鉴于PropertyDescriptors包含特定成员的setter和getter,访问器装饰器只能应用于单个成员的setter或getter,而不能同时应用于两者。 如果我们从访问器装饰器返回一个值,该值将成为getter和setter成员的访问器的新属性描述符。 下面是一个可用于更改gettersetter访问器的可枚举标志的装饰器示例:constenumerable(value:boolean){return(target:any,memberName:string,propertyDescriptor:PropertyDescriptor){propertyDescriptor。}} 请注意示例中,我们是如何使用装饰器工厂的。这允许我们在调用装饰器时指定可枚举标志。 以下是如何使用装饰器:classPerson{firstName:stringJonlastName:stringDoeenumerable(true)getfullName(){return{this。firstName}{this。lastName};}} 访问器装饰器类似于属性装饰器。唯一的区别是它们接收带有属性描述符的第三个参数。现在,我们已经创建了第一个访问器装饰器。 接下来,我们将学习如何创建方法装饰器。 创建方法装饰器 在这里,我们将学习如何使用方法装饰器。 方法装饰器的实现与创建访问器装饰器的方式非常相似。传递给装饰器实现的参数与传递给访问器装饰器的参数相同。 让我们重用之前创建的同一个可枚举装饰器,但这次是在以下Person类的getFullName方法中:constenumerable(value:boolean){return(target:any,memberName:string,propertyDescriptor:PropertyDescriptor){propertyDescriptor。}}classPerson{firstName:stringJonlastName:stringDoeenumerable(true)getFullName(){return{this。firstName}{this。lastName};}} 如果我们从方法装饰器返回一个值,该值将成为该方法的新属性描述符。 让我们创建一个deprecated的装饰器,它在使用该方法时将传递的消息打印到控制台,记录一条消息说该方法已被弃用:constdeprecated(deprecationReason:string){return(target:any,memberName:string,propertyDescriptor:PropertyDescriptor){return{get(){constwrapperFn(。。。args:any〔〕){console。warn(Method{memberName}isdeprecatedwithreason:{deprecationReason});propertyDescriptor。value。apply(this,args)}Object。defineProperty(this,memberName,{value:wrapperFn,configurable:true,writable:true});returnwrapperFn;}}}} 在这里,我们正在使用装饰器工厂创建装饰器。这个装饰器工厂接收一个字符串类型的参数,这是弃用的原因,如下面突出显示的部分所示:constdeprecated(deprecationReason:string){return(target:any,memberName:string,propertyDescriptor:PropertyDescriptor){。。。}} deprecationReason将在稍后将弃用消息记录到控制台时使用。在不推荐使用装饰器的实现中,我们正在返回一个值。当我们从方法装饰器返回值时,该值将覆盖该成员的属性描述符。 我们正在利用这一点为装饰类方法添加一个吸气剂。这样,我们就可以更改方法本身的实现。 但是为什么不直接使用Object。defineProperty而不是为方法返回一个新的属性装饰器呢?这是必要的,因为,我们需要访问this的值,对于非静态类方法,它绑定到类实例。 如果,我们直接使用Object。defineProperty,将无法检索this的值,并且如果该方法以任何方式使用this,则当从装饰器实现中运行包装的方法时,装饰器会破坏我们的代码。 在这样情况下,getter本身的this值绑定到非静态方法的类实例,并绑定到静态方法的类构造函数。 然后,在你的getter中创建一个本地包装函数,称为wrapperFn,此函数使用console。warn将消息记录到控制台,传递从装饰器工厂收到的deprecationReason,然后使用propertyDescriptor。value调用原始方法。 apply(this,args),以这种方式调用原始方法,并将其this值正确绑定到类实例,以防它是非静态方法。 然后,我们将使用defineProperty覆盖类中方法的值。这就像一种记忆机制,因为对同一方法的多次调用将不再调用getter,而是直接调用wrapperFn。 我们现在正在使用Object。defineProperty将类中的成员设置为将wrapperFn作为其值。 让我们使用已弃用的装饰器:constdeprecated(deprecationReason:string){return(target:any,memberName:string,propertyDescriptor:PropertyDescriptor){return{get(){constwrapperFn(。。。args:any〔〕){console。warn(Method{memberName}isdeprecatedwithreason:{deprecationReason});propertyDescriptor。value。apply(this,args)}Object。defineProperty(this,memberName,{value:wrapperFn,configurable:true,writable:true});returnwrapperFn;}}}}classTestClass{staticstaticMinstanceMember:stringhellodeprecated(Useanotherstaticmethod)staticdeprecatedMethodStatic(){console。log(insidedeprecatedstaticmethodstaticMember,this。staticMember);}deprecated(Useanotherinstancemethod)deprecatedMethod(){console。log(insidedeprecatedinstancemethodinstanceMember,this。instanceMember);}}TestClass。deprecatedMethodStatic();constinstancenewTestClass();instance。deprecatedMethod(); 在这里,我们创建了一个具有两个属性的TestClass:一个是静态的,一个是非静态的。我们还创建了两种方法:一种是静态的,一种是非静态的。 然后,我们将已弃用的装饰器应用于这两种方法。运行代码时,控制台中会出现以下内容:Output(warning)MethoddeprecatedMethodStaticisdeprecatedwithreason:UseanotherstaticmethodinsidedeprecatedstaticmethodstaticMembertrue(warning))MethoddeprecatedMethodisdeprecatedwithreason:UseanotherinstancemethodinsidedeprecatedinstancemethodinstanceMemberhello 这表明这两种方法都使用了包装函数正确包装,该函数将一条消息记录到控制台并说明弃用原因。 你现在已经使用TypeScript创建了你的第一个方法装饰器。 接下来,我们将学习如何创建TypeScript支持的最后一个装饰器类型,即参数装饰器。 创建参数装饰器 参数装饰器可以用在类方法的参数中。 在这里,我们将学习如何创建一个与参数一起使用的装饰器函数, 接收以下参数:对于静态属性,类的构造函数。对于所有其他属性,类的原型。成员的姓名。 方法参数列表中参数的索引。 无法更改与参数本身相关的任何内容,因此,此类装饰器仅对观察参数使用本身有用(除非您使用更高级的东西,例如反射元数据)。 这是一个装饰器的示例,它打印被装饰的参数的索引以及方法名称:functionprint(target:Object,propertyKey:string,parameterIndex:number){console。log(Decoratingparam{parameterIndex}from{propertyKey});} 然后,你可以像这样使用你的参数装饰器:classTestClass{testMethod(param0:any,printparam1:any){}} 运行上述代码应在控制台中显示以下内容:Decoratingparam1fromtestMethod 我们现在已经创建并执行了一个参数装饰器,并打印出返回装饰参数索引的结果。 总结 在本教程中,我们已经实现了TypeScript支持的所有装饰器,将它们与类一起使用,并了解了它们之间的区别。 现在可以开始编写自己的装饰器来减少代码库中的样板代码,或者更加自信地使用带有库(例如Mobx)的装饰器。 以上就是我跟你分享的全部内容,如果你觉得有用,请记得分享给你身边的朋友,也许能够帮助到他。