43618 次阅读

《TypeScript 中文入门教程》 17、注解

来源于: https://github.com/zhongsp/TypeScript/edit/master/doc/handbook/Decorators.md

介绍

随着TypeScript和ES6里引入了类,现在在一些场景下我们会需要额外的特性,用来支持标注或修改类及其成员。
Decorators提供了一种在类的声明和成员上使用元编程语法添加标注的方式。
Javascript里的Decorators目前处在建议征集的第一阶段,在TypeScript里做为实验性特性已经提供了支持。

注意  Decorators是实验性的特性,在未来的版本中可能会发生改变。

若要启用实验性的decorator,你必须启用experimentalDecorators编译器选项,在命令行中或在tsconfig.json

命令行:

tsc --target ES5 --experimentalDecorators

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

Decorators (后文译作装饰器)

装饰器是一种特殊类型的声明,它能够被附加到类声明方法访问符属性,或 参数上。
装饰器利用@expression这种方式,expression求值后必须为一个函数,它使用被装饰的声明信息在运行时被调用。

例如,有一个@sealed装饰器,我们会这样定义sealed函数:

function sealed(target) {
    // do something with "target" ...
}
注意  下面类装饰器小节里有一个更加详细的例子。

装饰器工厂

如果我们想自定义装饰器是如何作用于声明的,我们得写一个装饰器工厂函数。
装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。

我们可以通过下面的方式来写一个装饰器工厂

function color(value: string) { // 这是一个装饰器工厂
    return function (target) { //  这是装饰器
        // do something with "target" and "value"...
    }
}
注意  下面方法装饰器小节里有一个更加详细的例子。

装饰器组合

多个装饰器可以同时应用到一个声明上,就像下面的示例:

  • 写在同一行上:

    @f @g x
  • 写在多行上:

    @f
    @g
    x

当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合fg时,复合的结果(fg)(x)等同于f(g(x))。

同样的,在TypeScript里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:

  1. 由上至下依次对装饰器表达式求值。
  2. 求值的结果会被当作函数,由下至上依次调用。

如果我们使用装饰器工厂的话,可以通过下面的例子来观察它们求值的顺序:

function f() {
    console.log("f(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("f(): called");
    }
}

function g() {
    console.log("g(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("g(): called");
    }
}

class C {
    @f()
    @g()
    method() {}
}

在控制台里会打印出如下结果:

f(): evaluated
g(): evaluated
g(): called
f(): called

装饰器求值

类中不同声明上的装饰器将按以下规定的顺序应用:

  1. 参数装饰器,其次是方法访问符,或属性装饰器应用到每个实例成员。
  2. 参数装饰器,其次是方法访问符,或属性装饰器应用到每个静态成员。
  3. 参数装饰器应用到构造函数。
  4. 类装饰器应用到类。

类装饰器

类装饰器在类声明之前被声明(紧贴着类声明)。
类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。
类装饰器不能用在声明文件中(.d.ts),也不能用在任何外部上下文中(比如declare的类)。

类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。

如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

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

下面是使用类装饰器(@sealed)的例子,应用到Greeter类:

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

我们可以这样定义@sealed装饰器

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed被执行的时候,它将密封此类的构造函数和原型。(注:参见Object.seal)

方法装饰器

方法装饰器声明在一个方法的声明之前(紧贴着方法声明)。
它会被应用到方法的属性描述符上,可以用来监视,修改或者替换方法定义。
方法装饰器不能用在声明文件(.d.ts),重载或者任何外部上下文(比如declare的类)中。

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符
注意  如果代码输出目标版本小于ES5Property Descriptor将会是undefined

如果方法装饰器返回一个值,它会被用作方法的属性描述符

注意  如果代码输出目标版本小于ES5返回值会被忽略。

下面是一个方法装饰器(@enumerable)的例子,应用于Greeter类的方法上:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}

我们可以用下面的函数声明来定义@enumerable装饰器:

function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}

这里的@enumerable(false)是一个装饰器工厂
当装饰器@enumerable(false)被调用时,它会修改属性描述符的enumerable属性。

访问符装饰器

访问符装饰器声明在一个访问符的声明之前(紧贴着访问符声明)。
访问符装饰器应用于访问符的属性描述符并且可以用来监视,修改或替换一个访问符的定义。
访问符装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如declare的类)里。

注意  TypeScript不允许同时装饰一个成员的getset访问符。相反,所有装饰的成员必须被应用到文档顺序指定的第一个访问符。这是因为,装饰器应用于一个属性描述符,它联合了getset访问符,而不是分开声明的。

访问符装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符
注意  如果代码输出目标版本小于ES5Property Descriptor将会是undefined

如果访问符装饰器返回一个值,它会被用作方法的属性描述符

注意  如果代码输出目标版本小于ES5返回值会被忽略。

下面是使用了访问符装饰器(@configurable)的例子,应用于Point类的成员上:

class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }

    @configurable(false)
    get x() { return this._x; }

    @configurable(false)
    get y() { return this._y; }
}

我们可以通过如下函数声明来定义@configurable装饰器:

function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

属性装饰器

属性装饰器声明在一个属性声明之前(紧贴着属性声明)。
属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如declare的类)里。

属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
注意  属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。
因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。
因此,属性描述符只能用来监视类中是否声明了某个名字的属性。

如果属性装饰器返回一个值,它会被用作方法的属性描述符

注意  如果代码输出目标版本小于ES5,返回值会被忽略。

如果访问符装饰器返回一个值,它会被用作方法的属性描述符

我们可以用它来记录这个属性的元数据,如下例所示:

class Greeter {
    @format("Hello, %s")
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        let formatString = getFormat(this, "greeting");
        return formatString.replace("%s", this.greeting);
    }
}

然后定义@format装饰器和getFormat函数:

import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

这个 @format("Hello, %s") 装饰器是个 装饰器工厂
@format("Hello, %s")被调用时,它添加一条这个属性的元数据,通过reflect-metadata库里的Reflect.metadata函数。
getFormat被调用时,它读取格式的元数据。

注意  这个例子需要使用reflect-metadata库。
查看元数据了解reflect-metadata库更详细的信息。

参数装饰器

参数装饰器声明在一个参数声明之前(紧贴着参数声明)。
参数装饰器应用于类构造函数或方法声明。
参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如declare的类)里。

参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 参数在函数参数列表中的索引。
注意  参数装饰器只能用来监视一个方法的参数是否被传入。

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

下例定义了参数装饰器(@required)并应用于Greeter类方法的一个参数:

class Greeter {
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }

    @validate
    greet(@required name: string) {
        return "Hello " + name + ", " + this.greeting;
    }
}

然后我们使用下面的函数定义 @required@validate 装饰器:

import "reflect-metadata";

const requiredMetadataKey = Symbol("required");

function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
    let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
    existingRequiredParameters.push(parameterIndex);
    Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}

function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
    let method = descriptor.value;
    descriptor.value = function () {
        let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
        if (requiredParameters) {
            for (let parameterIndex of requiredParameters) {
                if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
                    throw new Error("Missing required argument.");
                }
            }
        }

        return method.apply(this, arguments);
    }
}

@required装饰器添加了元数据实体把参数标记为必须的。
@validate装饰器把greet方法包裹在一个函数里在调用原先的函数前验证函数参数。

注意  这个例子使用了reflect-metadata库。
查看元数据了解reflect-metadata库的更多信息。

元数据

一些例子使用了reflect-metadata库来支持实验性的 metadata API
这个库还不是ECMAScript (JavaScript)标准的一部分。
然而,当装饰器被ECMAScript官方标准采纳后,这些扩展也将被推荐给ECMAScript以采纳。

你可以通过npm安装这个库:

npm i reflect-metadata --save

TypeScript支持为带有装饰器的声明生成元数据。
你需要在命令行或tsconfig.json里启用emitDecoratorMetadata编译器选项。

Command Line:

tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

当启用后,只要reflect-metadata库被引入了,设计阶段额外的信息可以在运行时使用。

如下例所示:

import "reflect-metadata";

class Point {
    x: number;
    y: number;
}

class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }
}

function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
    let set = descriptor.set;
    descriptor.set = function (value: T) {
        let type = Reflect.getMetadata("design:type", target, propertyKey);
        if (!(value instanceof type)) {
            throw new TypeError("Invalid type.");
        }
    }
}

TypeScript编译器可以通过@Reflect.metadata装饰器注入设计阶段的类型信息。
你可以认为它相当于下面的TypeScript:

class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    @Reflect.metadata("design:type", Point)
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    @Reflect.metadata("design:type", Point)
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }
}
注意  装饰器元数据是个实验性的特性并且可能在以后的版本中发生破坏性的改变(breaking changes)。

相关文章

已有 706 条评论
  1. MiaWig

    lisinopril where to buy tetracycline buy ventolin without prescription prednisolone phenergan tablets

    MiaWig 回复
  2. JimWig

    ventolin inhaler without prescription lisinopril propecia phenergan 12.5 prednisolone buy online viagra prescription tetracycline for acne metformin tadalafil without prescription buy cialis from canada

    JimWig 回复
  3. AlanWig

    tetracycline online viagra pills online cheap finasteride ventolin price phenergan promethazine

    AlanWig 回复
  4. SamWig

    buy tadalafil cialis buy cialis online canada phenergan 50 mg metformin generic lisinopril 5mg tetracycline buy viagra online without prescription propecia prednisolone proventil inhaler for sale

    SamWig 回复
  5. insurance auto

    best auto insurance rates

    insurance auto 回复
  6. IvyWig

    prednisolone tablets no prescription lisinopril tetracycline without prescriptions no prescription cialis buy metformin online

    IvyWig 回复
  7. AnnaWig

    buy cialis online canada buy tadalafil tetracycline propecia lisinopril buy prednisolone tablets phenergan viagra without prescription generic metformin ventolin without prescription

    AnnaWig 回复
  8. JackWig

    propecia tetracycline purchase cialis canada pharmacy phenergan drug prednisolone 20 mg buy cheap tadalafil ventolin inhaler non prescription lisinopril 5mg tab where can i get viagra generic metformin

    JackWig 回复
  9. EvaWig

    lisinopril without prescription

    EvaWig 回复
  10. go auto insurance

    compare auto insurance rates

    go auto insurance 回复
  11. JoeWig

    tadalafil no prescription methyl prednisolone tetracycline 500 mg where to buy cialis online phenergan buy buy lisinopril without prescription buy propecia 5mg viagra without prescription ventolin generic metformin

    JoeWig 回复
  12. motorcycle insurance

    best and cheapest auto insurance

    motorcycle insurance 回复
  13. JaneWig

    phenergan

    JaneWig 回复
  14. EvaWig

    lisinopril without prescription

    EvaWig 回复
  15. IvyWig

    buy tadalafil propecia tetracycline metformin order female viagra

    IvyWig 回复
  16. JasonWig

    prednisolone buy lisinopril without prescription phenergan 50 mg buy propecia without a prescription viagra without prescription

    JasonWig 回复
  17. JasonWig

    order phenergan without prescription ventolin inhaler no prescription lisinopril prescription for viagra tetracycline

    JasonWig 回复
  18. MarkWig

    prednisolone tadalafil tetracycline prescription viagra 150 mg cialis from canada

    MarkWig 回复
  19. SamWig

    prednisolone buy online ventolin propecia tadalafil tetracycline pill buy cialis from canada buy phenergan prescription for viagra lisinopril 20 mg no prescription cheap metformin

    SamWig 回复
  20. LisaWig

    buy metformin online

    LisaWig 回复
发表新评论