首页 TypeScript 剖析 TypeScript 修饰器及其应用模式

剖析 TypeScript 修饰器及其应用模式

0 2.1K

本文我们将学习 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 对属性描述符的支持并不完善。

访问器修饰器

乍看起来,访问器修饰器和方法修饰器并无不同。当静态或实例方法加上getset前缀,它们就变成了访问器。如果方法具有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是一个属性,其属性描述符同时具有getset字符,保存各自得函数体。

因此,尽管为这些访问器分别提供相同或不同的修饰器看起来很公平,但 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访问器的属性描述符。这个属性描述符同时具有getset字段。

属性修饰器

我们还可以修饰类的静态或实例属性。属性修饰器函数接受两个参数。第一个参数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函数,给修饰对象targetfnamelname属性别分定义了一个元数据case。这个元数据的值是一个字符串,保存该属性应该是怎样的格式。所以,fname应该转换成大写,lname则转换成小写。

之后,我们在fullname的 getter 访问器中,通过Reflect.getMetadata函数读取这个元数据。由于this在实例方法中指向本身,this和修饰器函数中的target并不是一样的(targetPerson.prototype)。

Reflect.getMetadata(key, target, prop)同样有一个target参数,也就是this的原型,即Person.prototype,因此,它能够读取元数据的值。但Reflect.getOwnMetadata是不能读取到的。

@Reflect.metadata(metadataKey, metadataValue)

Reflect.metadata返回修饰器函数。因此,其实并不需要自己编写类似textCase这样的修饰器。

由这个函数返回的修饰器适用于任意对象,所以你可以使用它修饰类、函数、访问器等。这个修饰器可以给targettarget的属性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对于构造函数始终返回undefinedtarget则返回类本身,而不是原型。

利用index参数,我们可以给每个参数添加元数据。由于元数据值是根据参数位置识别的,所以可以使用01,并且将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一样。这里我们把所有能修饰的实体都加上了修饰器。从输出结果可以看出修饰的顺序。

  1. 首先是实例属性,紧接着是实例方法参数、实例方法,最后是实例访问器;
  2. 然后是静态属性,之后是静态方法参数、静态方法,最后是静态访问器;
  3. 然后是构造函数参数;
  4. 最后是类。

发出修饰器元数据

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会被序列化为Stringnumber序列化为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 编译器在编译期发现不存在这个属性。这一问题在这里有详细阐述。

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2