原文链接:https://medium.com/jspoint/anatomy-of-typescript-decorators-and-their-usage-patterns-487729b34ae6
本文我们将学习 TypeScript 的修饰器模式,以及修饰器是如何改变一个类的。同时,我们也将了解到 reflect-metedata 包是如何帮助我们设计修饰器的。
修饰器是一种注解,放置在类声明或类成员变量之前,用来改变类或属性的行为。如果你是 Angular 开发者,那么就会知道定义 Angular 组件的 @Component
修饰器。
@Component({ selector: 'app-product-item', templateUrl: './product-item.component.html', styleUrls: ['./product-item.component.css'] }) export class ProductItemComponent { @Input() item: Product; }
上面的例子,@Component
注解就是一个修饰器,用于修饰ProductItemComponent
类。它将这个类变成 Angular 组件,组件的信息则通过修饰器注解获取。类似的,@Input
也是一个修饰器,修饰的是类的实例属性。
前面我们介绍过 JavaScript 元编程。简单来说,元编程就是一种编程模式,可以内省和控制程序的行为。例如,@Component
修饰器改变了ProductItemComponent
类的行为。
本质上说,修饰器就是一个 JavaScript 函数。但是,当这个函数放在类或成员前面,以@
开始时,它就会在运行时被调用,同时传入某些特定的参数。这些参数代表了它所修饰的类或成员的内部信息。在这个函数中,我们可以改变这些内部信息,从而改变程序的行为。
与枚举或接口不同,修饰器并不是 TypeScript 的特性。它就是纯粹的 JavaScript 特性,但是还没有被标准化。该提案目前处于 stage-3。不过,利用 babel 插件,我们现在就可以在 JavaScript 中使用修饰器。在前面的章节,我们仔细探讨了如何通过 babel 命令行转译修饰器。
TypeScript 在提案相当早期就实现了修饰器,这意味着目前版本的提案与 TypeScript 实现的版本已经相距甚远。即使这样,TypeScript 实现的所谓修饰器提案的传统版本同样是相当有趣,也很有用。
TypeScript 现在并不急于实现修饰器提案中的修改。因为这些改变会引入破坏性变化,没人知道到底多少第三方库会因此受到影响。我认为 TypeScript 会等到提案完成标准化之后,才去考虑进一步的修改。
由于修饰器并不是 ECMAScript 标准,现阶段只是作为实验性功能,TypeScript 要求必须显式启用。因此,你需要在 tsconfig.json 中将编译参数experimentalDecorator
设置为true
,或者在编译命令添加--experimentalDecorator
标记才可以。
{ "files": [ "program.ts" ], "compilerOptions": { "target": "ES6", "experimentalDecorators": true, "removeComments": true, "alwaysStrict": true } }
上面的例子中,我们将target
级别设置为ES6
,方便我们更好理解修饰器编译之后的代码。但并不意味着你不能选择其它级别。事实上,TypeScript 会根据选择的target
确定编译后的代码。
修饰器只能修饰类或其成员(属性、方法、访问器等)。TypeScript 支持类声明修饰器、方法修饰器、访问器修饰器(getter/setter),方法参数修饰器(包括构造函数)以及类属性修饰器。
类声明修饰器
前面我们看到的@Component
就是一个类声明修饰器,或者也可以简单称为类修饰器。它可以修改类本身。修饰器就是一个 JavaScript 函数。现在,我们创建一个简单的修饰器,用于冻结类及其属性。
// lock decorator for classes function lock( ctor: Function ) { Object.freeze( ctor ); Object.freeze( ctor.prototype ); } @lock class Person { static version: string = 'v1.0.0'; fname: string; lname: string; constructor( fname: string, lname: string ) { this.fname = fname; this.lname = lname; } getFullName(): string { return this.fname + ' ' + this.lname; } } // add properties to class Person.version = 'v1.0.1'; // ❌ Error => TypeError: Cannot assign to read only property 'version' of function 'class Person' // add properties to class prototype Person.prototype.getFullName = null; // ❌ Error => TypeError: Cannot assign to read only property 'getFullName' of object '#<Person>'
在上面的例子中,我们创建了一个类Person
。这是一个简单的 JavaScript(ES6)类,有一些static
属性和实例属性、实例方法,没什么特别的。但是,我们给这个类加了个@lock
修饰器。
正如前面我们所说的,修饰器lock
就是一个 JavaScript 函数。这个函数由 JavaScript 引擎在运行时调用。当lock
函数执行时,它的唯一参数是它所修饰的这个类的构造函数。
这个参数让好多人有些迷惑,因为我们声明的是类,而参数接收到的却是构造函数。怎么回事?这是由于,ES6 中的class
是一个很有趣的关键字,用于创建类。但是底层上(JavaScript 引擎中),类就是构造函数和原型。所以,上面的代码与下面的其实是类似的。
function Person(fname, lname) { this.fname = fname; this.lname = lname; } Person.version = 'v1.0.0'; Person.prototype.getFullName = function() { return this.fname + ' ' + this.lname; }
所以,我们之前所说的Person
的构造函数,其实就是上面的Person
函数。这正是我们在修饰器函数中接收到的那个参数。它也有prototype
作为static
属性。所以,当别人说起class
的时候,就把它理解成一个构造函数,这个函数体就是class.constructor
的函数体,构造函数的原型则包含类的所有实例方法。
在lock
修饰器在,使用Object.freeze()
函数,将构造函数及其原型全部冻结,因此,我们不能在运行时新增或修改任何属性,否则就会看到上面注释中的错误。
你一定会好奇,JavaScript 并不支持@
前缀,这种写法怎么还能起作用呢?这是因为,当编译这段代码时,TypeScript 编译器会移除修饰器名字前面的@
前缀,然后将修饰器替换为一个工具函数,由这个函数执行修饰器的代码。
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; function lock(ctor) { Object.freeze(ctor); Object.freeze(ctor.prototype); } let Person = (() => { let Person = class Person { constructor(fname, lname) { this.fname = fname; this.lname = lname; } getFullName() { return this.fname + ' ' + this.lname; } }; Person.version = 'v1.0.0'; Person = __decorate([ lock ], Person); return Person; })(); Person.prototype.getFullName = null;
上面的例子是之前的代码通过 tsc 编译后的文件。这里,__decorate
就是一个工具函数,用于调用在Person
类上lock
修饰器函数。
现在,我们的lock
修饰器没有返回任何值。如果类修饰器返回了一个值,那么,这个值就会替代它所修饰的原始构造函数(或者称为原始类),因此,这个返回值必须是一个新的构造函数(或称为类)。此时,你有责任接管维护原始类的原型。
function decorator(class) { // modify 'class' or return a new one }
// `Ctor` type presents a constructor function interface Ctor { new (...args: any[] ): any; } // return a new class that extends incoming class function withInfo<T extends Ctor>( ctor: T ): T { return class NewCtor extends ctor { info(): string { return `An instance of a "${ ctor.name }".`; } }; } @withInfo class Person { static version: string = 'v1.0.0'; fname: string; lname: string; constructor( fname: string, lname: string ) { this.fname = fname; this.lname = lname; } getFullName(): string { return this.fname + ' ' + this.lname; } } const person = new Person( 'Ross', 'Geller' ); console.log( 'version ->', Person.version ); // version -> v1.0.0 console.log( 'fullname ->', person.getFullName() ); // fullname -> Ross Geller console.log( 'info ->', (person as any).info() ); // info -> An instance of a "Person".
上面的例子,withInfo
修饰器是一个泛型函数。类型参数T
表示类的静态类型,等价于:typeof Person
注解。TypeScript 隐式地将修饰器所修饰的类的这个类型信息提供给修饰器函数。泛型约束T extends Ctor
意味着只有传入的类型是类时,修饰器才可用。这意味着这个修饰器只能用于类,不能用于内部成员。
在withInfo
修饰器函数内部,我们返回一个继承了Person
的新类。这个新的类没有构造函数,这意味着Person
的构造函数被隐式调用。person
对象及其原型链类似下面。
方法修饰器
方法修饰器用于修饰类中除构造函数以外的其它静态或实例函数。方法修饰器函数接受三个参数。第一个参数是target
,表示该方法属于哪个对象。如果修饰的方法是static
的,这个参数是构造函数(类);如果修饰的方法是实例方法,则这个参数是类的原型。
function decorator(target, name, descriptor) { // modify 'descriptor' or return new one }
第二个参数是方法名,第三个参数是方法的属性描述符。修饰器函数不必须返回值;但如果有返回值,这个返回值就应该是方法的新的属性描述符,会被用来替代已有的属性描述符。
// make property readonly function readonly( target, name, desc ) { desc.writable = false; } // adds `v` prefix function prefix( target, name, desc ) { return { writable: false, enumerable: false, configurable: false, value: () => `v${ desc.value() }`, }; } class Person { constructor( public fname: string, public lname: string ) {} @readonly getFullName(): string { return this.fname + ' ' + this.lname; } @prefix static getVersion() { return '1.0.0'; } } // get version console.log( 'version ->', Person.getVersion() ); // ✅ v1.0.0 // override `getFullName` method Person.prototype.getFullName = null; // ❌ TypeError: Cannot assign to read only property 'getFullName' of object '#<Person>'
在上面的例子中,readonly
修饰器函数只修改属性描述符的writable
设置。prefix
修饰器函数返回一个新的属性描述符。注意,这个新的属性描述符的value
字段包含了方法实现的函数体。
当编译目标设置为ES3
时,修饰器函数不会接收第三个参数。同时,其返回值也会被忽略。这是因为 ES3 对属性描述符的支持并不完善。
访问器修饰器
乍看起来,访问器修饰器和方法修饰器并无不同。当静态或实例方法加上get
或set
前缀,它们就变成了访问器。如果方法具有get
前缀(getter 函数),它的属性描述符会使用get
字段而不是value
保存函数体。类似的,setter 函数的函数体保存在set
字段。
class Person { constructor( public fname: string, public lname: string ) {} get fullname(): string { return this.fname + ' ' + this.lname; } set fullname( name ) { [ this.fname, this.lname ] = name.split(' '); } }
具有相同名字的 getter 和 setter 函数没有分开的属性描述符。例如上面的例子,fullname
是一个属性,其属性描述符同时具有get
和set
字符,保存各自得函数体。
因此,尽管为这些访问器分别提供相同或不同的修饰器看起来很公平,但 TypeScript 并不建议这么做。你可以在第一个访问器上面添加一个修饰器,同时修饰两个访问器。看下面的例子。
// convert accessor to uppercase function uppercase( target, name, desc ) { return { enumerable: false, configurable: false, get: function () { return desc.get.call( this ).toUpperCase(); }, set: function ( name ) { desc.set.call( this, name.split(' ') ); } }; } class Person { constructor( public fname: string, public lname: string, ) {} @uppercase get fullname(): string { return this.fname + ' ' + this.lname; } set fullname( [ fname, lname ] ) { this.fname = fname; this.lname = lname; } } var person = new Person( 'Ross', 'Geller' ); console.log( 'fullname ->', person.fullname ); // fullname -> ROSS GELLER console.log( 'person ->', person ); // person -> { fname: 'Ross', lname: 'Geller' } person.fullname = 'Chandler Bing'; console.log( 'fullname* ->', person.fullname ); // fullname* -> CHANDLER BING console.log( 'person* ->', person ); // person* -> { fname: 'Chandler', lname: 'Bing' }
在上面的例子,使用uppercase
修饰器函数,我们返回一个新的fullname
访问器的属性描述符。这个属性描述符同时具有get
和set
字段。
属性修饰器
我们还可以修饰类的静态或实例属性。属性修饰器函数接受两个参数。第一个参数target
,如果是static
属性,即该类的构造函数;如果是实例属性,则是该类的原型。第二个参数是属性的名字。
function decorator(target, name) { // collect or store some information }
这个修饰器有一点不同。实例属性(也就是字段)是在实例创建时在实例上生成的。因此,我们并没办法真正配置类的实例属性的属性描述符。TypeScript 没有提供足够的机制去修饰类的属性。这里提到的修饰器提案正是我们所需要的解决方案。
所以,TypeScript 中,属性修饰符函数并不使用属性描述符作为其参数。同时,修饰器函数的返回值也会被忽略。那我们为什么还需要属性描述符呢?这里,属性描述符可以获取属性的信息。
在前面的文章中,我们讨论了 Reflect Metadata 提案;reflect-metadata 包即这个提案的兼容方案。如果你没有阅读这篇文章,在继续学习之前,最好先了解一下。
Reflect.defineMetadata(key, value, target, prop)
给target
对象或其属性prop
定义了具有唯一键key
的元数据,其值为value
。使用Reflect.getMetadata
则可以读取元数据。
import 'reflect-metadata'; // decorator to save metadata of a property function textCase( target, name ) { const tcase = name === 'fname' ? 'upper' : 'lower'; Reflect.defineMetadata( 'case', tcase, target, name ); } // get text case from metadata function getTextCase( target, name ) { return Reflect.getMetadata( 'case', target, name ); } class Person { @textCase public fname: string; @textCase public lname: string; constructor( fname: string, lname: string ) { this.fname = fname; this.lname = lname; } get fullname(): string { const fnameCase = getTextCase( this, 'fname' ); const lnameCase = getTextCase( this, 'lname' ); const fname = 'upper' === fnameCase ? this.fname.toUpperCase() : this.fname.toLowerCase(); const lname = 'upper' === lnameCase ? this.lname.toUpperCase() : this.lname.toLowerCase(); return fname + ' ' + lname; } } var person = new Person( 'Ross', 'Geller' ); console.log( person.fullname ); // ROSS geller
上面的例子,textCase
修饰器函数使用Reflect.defineMetadata
函数,给修饰对象target
的fname
和lname
属性别分定义了一个元数据case
。这个元数据的值是一个字符串,保存该属性应该是怎样的格式。所以,fname
应该转换成大写,lname
则转换成小写。
之后,我们在fullname
的 getter 访问器中,通过Reflect.getMetadata
函数读取这个元数据。由于this
在实例方法中指向本身,this
和修饰器函数中的target
并不是一样的(target
是Person.prototype
)。
Reflect.getMetadata(key, target, prop)
同样有一个target
参数,也就是this
的原型,即Person.prototype
,因此,它能够读取元数据的值。但Reflect.getOwnMetadata
是不能读取到的。
@Reflect.metadata(metadataKey, metadataValue)
Reflect.metadata
返回修饰器函数。因此,其实并不需要自己编写类似textCase
这样的修饰器。
由这个函数返回的修饰器适用于任意对象,所以你可以使用它修饰类、函数、访问器等。这个修饰器可以给target
或target
的属性property
添加元数据。这是通过调用Reflect.defineMetadata
实现的。
import 'reflect-metadata'; // get text case from metadata function getTextCase( target, name ) { return Reflect.getMetadata( 'case', target, name ); } class Person { @Reflect.metadata( 'case', 'upper' ) public fname: string; @Reflect.metadata( 'case', 'lower' ) public lname: string; constructor( fname: string, lname: string ) { this.fname = fname; this.lname = lname; } get fullname(): string { const fnameCase = getTextCase( this, 'fname' ); const lnameCase = getTextCase( this, 'lname' ); const fname = 'upper' === fnameCase ? this.fname.toUpperCase() : this.fname.toLowerCase(); const lname = 'upper' === lnameCase ? this.lname.toUpperCase() : this.lname.toLowerCase(); return fname + ' ' + lname; } } var person = new Person( 'Ross', 'Geller' ); console.log( person.fullname ); // ROSS geller
Reflect.metadata
看起来和textCase
一样。它接受一个元数据的键和值,返回的修饰器函数可以给target
或其属性property
添加元数据。
参数修饰器
参数修饰器可以修饰函数参数,包括构造函数的参数。参数修饰器函数接受三个参数。第一个target
,如果修饰的参数所在函数是static
的,为构造函数;如果所在函数是实例方法,则是该类的原型。
第二个参数是方法名字;第三个参数是在方法声明中,该参数出现的位置。
function decorator(target, name, index) { // collect or store some information }
由于我们谈论的是参数而不是属性,因此不存在接收或返回属性描述符的问题。像参数装饰器一样,这些装饰器也用于收集或存储有关参数的某些元数据。
import 'reflect-metadata'; // save metadata of a parameter with its index function textCase( target, name, index ) { const tcase = index === 0 ? 'upper' : 'lower'; Reflect.defineMetadata( `case_${index}`, tcase, target, name ); } // get text case from metadata function getTextCase( target, name, index ) { return Reflect.getMetadata( `case_${index}`, target, name ); } class Person { public fname: string; public lname: string; constructor( @textCase fname: string, @textCase lname: string ) { this.fname = fname; this.lname = lname; } get fullname(): string { const fnameCase = getTextCase( Person, undefined, 0 ); const lnameCase = getTextCase( Person, undefined, 1 ); const fname = 'upper' === fnameCase ? this.fname.toUpperCase() : this.fname.toLowerCase(); const lname = 'upper' === lnameCase ? this.lname.toUpperCase() : this.lname.toLowerCase(); return fname + ' ' + lname; } } var person = new Person( 'Ross', 'Geller' ); console.log( person.fullname ); // ROSS geller
上面的例子,textCase
修饰器修饰了构造函数的参数。在修饰器函数中,参数name
对于构造函数始终返回undefined
;target
则返回类本身,而不是原型。
利用index
参数,我们可以给每个参数添加元数据。由于元数据值是根据参数位置识别的,所以可以使用0
、1
,并且将undefined
作为方法名,在fullname
的 getter 访问器中获取元数据。
更好地使用修饰器
修饰器工厂
修饰器工厂是返回修饰器的函数。所以,之前我们提到的Reflect.metadata
就是一个修饰器工厂,因为它返回了一个修饰器函数。当你想要使用修饰器工厂修饰某些实体时,我们需要使用函数调用的语法,例如decoFactory(...args)
。
// decorator factory function version( version: string ) { return function( target: any ) { target.version = version; } } @version('v1.0.1') class Person { fname: string; lname: string; constructor( fname: string, lname: string ) { this.fname = fname; this.lname = lname; } getFullName(): string { return this.fname + ' ' + this.lname; } } // log class version console.log( 'version ->', (Person as any).version ); // version -> v1.0.1
上面的例子中,version
函数是一个修饰器工厂。因此,我们需要使用@version(...)
去调用这个函数,以便修饰这个类。
修饰器工厂对于需要自定义修饰器的情形很有用。你会发现,大部分场景中,修饰器工厂都比直接修饰器函数本身更常见,因为修饰器工厂提供了自定义的能力,并且可以复用。
修饰器链
多个修饰器可以组成修饰器链。为一个实体添加两个或多个修饰器有两种方法。可以像下面代码那样,从上往下依次添加修饰器。此时,修饰器会从上往下执行,但是会从下往上应用。
@decoratorA @decoratorB entity
也可以将多个修饰器并排写出来。这样的话,这些修饰器会从左往右执行,但是会从右往左应用。
@decoratorA @decoratorB entity
在上面两个例子中,decoratorA
第一个执行,所以,如果decoratorA
是一个修饰器工厂,修饰器函数就会被返回。一旦所有修饰器都被执行,它们会按照相反的方向去应用。因此,decoratorB
会先被应用,然后是decoratorA
。下面是一个例子。
// decorator factory function decoratorFactory( label: string ) { console.log( 'factory():', label ); return function( ...args: any[] ) { console.log( 'decorator():', label ); } } // decorator function decorator( ...args: any[] ) { console.log( 'decorator():', 'param-B' ); } @decoratorFactory('class-A') @decoratorFactory('class-B') @decoratorFactory('class-C') class Person { constructor( @decoratorFactory('param-A') @decorator @decoratorFactory('param-C') public name: string ) {} }
运行结果如下:
正如上面所见,工厂函数按照修饰器添加的顺序依次执行,但是真正的修饰器按照相反的方向应用的。当然,我们也可以混合修饰器工厂和修饰器函数,就像上面name
参数的修饰器那样。
修饰的顺序
在上面的例子中,我们清楚地看到,类修饰器在参数修饰器之前执行,但是参数修饰器却是第一个应用的。下面我们看看不同类型的修饰器的执行顺序和应用顺序。
// decorator factory function factory( label: string ) { console.log( 'factory():', label ); return function( ...args: any[] ) { console.log( 'decorator():', label ); } } @factory('class') class Person { @factory('property-instance') name: string; @factory('property-static') static version: string; constructor( @factory('param-constructor') name: string ) { this.name = name } @factory('method-instance') getName( @factory('param-instance') prefix: string ) { return prefix + ' ' + this.name; } @factory('getter-instance') personName() { return this.name;} @factory('method-static') static getVersion( @factory('param-static') prefix: string ) { return prefix + '' + this.version; } @factory('getter-static') static personVersion() { return this.version;} }
在上面的例子中,factory
修饰器工厂与之前的decoratorFactory
一样。这里我们把所有能修饰的实体都加上了修饰器。从输出结果可以看出修饰的顺序。
- 首先是实例属性,紧接着是实例方法参数、实例方法,最后是实例访问器;
- 然后是静态属性,之后是静态方法参数、静态方法,最后是静态访问器;
- 然后是构造函数参数;
- 最后是类。
发出修饰器元数据
TypeScript 提供了emitDecoratorMetadata
编译器参数(或者是--emitDecoratorMetadata
编译器标记),用于隐式向类或属性添加特定的元数据。当该参数启用时,对于使用了修饰器的代码,TypeScript 编译器会向最终生成的 JavaScript 代码添加额外的代码。
{ "files": [ "program.ts" ], "compilerOptions": { "target": "ES6", "experimentalDecorators": true, "emitDecoratorMetadata": true, "removeComments": true, "alwaysStrict": true } }
import 'reflect-metadata'; // decorator function that does nothig function noop(...args: any[]) {} @noop class Person { constructor( public name: string, public age: number, ) {} getAge(): string { return this.age.toString(); } @noop getNameWithPrefix(prefix: string): string { const type = Reflect.getMetadata( 'design:type', this, 'getNameWithPrefix' ); console.log( 'type ->', type ); // type -> [Function: Function] const paramtypes = Reflect.getMetadata( 'design:paramtypes', this, 'getNameWithPrefix' ); console.log( 'paramtypes ->', paramtypes ); // paramtypes -> [ [Function: String] ] const returntype = Reflect.getMetadata( 'design:returntype', this, 'getNameWithPrefix' ); console.log( 'returntype ->', returntype ); // returntype -> [Function: String] return prefix + ' ' + this.name; } } var person = new Person( 'Ross Geller', 29 ); person.getNameWithPrefix( 'Dr.' );
上面的代码中,我们创建了一个什么都不做的修饰器noop
。然后,我们使用这个修饰器修饰Person
类和getNameWithPrefix
方法。在getNameWithPrefix
方法中,我们使用Reflect.getMetadata
读取到了一些奇怪的元数据。这些元数据是什么时候添加的呢?
为了理解这些元数据是在哪里添加的,我们需要看看编译出的 JavaScript 代码,因为添加元数据的逻辑是在编译期,由 TypeScript 编译器加入的。
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); require("reflect-metadata"); function noop(...args) { } let Person = (() => { let Person = class Person { constructor(name, age) { this.name = name; this.age = age; } getAge() { return this.age.toString(); } getNameWithPrefix(prefix) { const type = Reflect.getMetadata('design:type', this, 'getNameWithPrefix'); console.log('type ->', type); const paramtypes = Reflect.getMetadata('design:paramtypes', this, 'getNameWithPrefix'); console.log('paramtypes ->', paramtypes); const returntype = Reflect.getMetadata('design:returntype', this, 'getNameWithPrefix'); console.log('returntype ->', returntype); return prefix + ' ' + this.name; } }; __decorate([ noop, __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", String) ], Person.prototype, "getNameWithPrefix", null); Person = __decorate([ noop, __metadata("design:paramtypes", [String, Number]) ], Person); return Person; })(); var person = new Person('Ross Geller', 29); person.getNameWithPrefix('Dr.');
在这段代码中,TypeScript 自动对getNameWithPrefix
方法和Person
类添加了额外几个修饰器,连同noop
一起。这是通过__metadata
辅助函数实现的。该函数就是一个修饰器工厂。
这个修饰器工厂返回一个修饰器,该修饰器利用Reflect.metadata
函数,为给定对象或其属性保存元数据。这些修饰器只应用于添加了修饰器的类或类成员。这些修饰器扮演的角色分别是:
- 为实体保存运行时数据类型,元数据的键为
design:type
; - 为方法参数保存运行时数据类型,元数据的键为
design:paramtypes
; - 为方法返回值保存运行时数据类型,元数据的键为
design:returntype
。
这里,运行时数据类型就是元数据的值。这些数据类型由 TypeScript 类型序列化而来。例如,string
会被序列化为String
,number
序列化为Number
。在这里可以了解更多关于数据类型序列化的信息。
注意事项
现在我们知道了,修饰器用于改变类或其成员的行为。TypeScript 实现这一目的的方式是,通过一系列辅助函数在运行时修改类。
如果类被声明为外部的(使用declare
关键字),或者在类型声明文件中被声明,那么,关于修饰器的生成代码就不会生成,因为这是没有意义的,并且类型声明文件不会有任何输出。
另外,注意上面示例中,有些代码我们使用了Person as any
类型断言。例如,对于类修饰器的例子中,version
修饰器为Person
类添加了version
静态属性。但如果你试图在Person
类访问version
属性,TypeScript 会报错:
Property 'version' does not exist on type 'typeof Person'.
console.log( 'version ->', Person.version );
这是因为version
属性是在运行时添加到Person
类的,所以 TypeScript 编译器在编译期发现不存在这个属性。这一问题在这里有详细阐述。