一、说明

TypeScript 和 ES6 都引入了 class,在一些场景下可能需要额外的特性来支持标注或者是修改及其成员。装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。

JavaScript 的装饰器目前处于 建议征集第二阶段 ,不过 TypeScript 已经可以在实验性特性中使用。

注意:装饰器是一项实验性特性,在未来的版本中可能会发生变化

如果要启用实验性的装饰器特性,可以在命令行或者 tsconfig.json 里启用 experimentalDecoratiors 编译器选项:

命令行

tsc --target ES5 --experimentalDecorators

tsconfig.json

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

二、装饰器

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

比如:有一个 @sealed 装饰器,定义 sealed 函数如下:

function sealed(target) {
    //.....
}

装饰器工厂

如果要定制一个装饰器,这个装饰器如何应用到一个声明上?

这时候需要写一个装饰器工厂函数,装饰器工厂 就是一个简单的函数,它返回一个表达式,以便于装饰器在运行时调用。

比如通过以下方式来写一个装饰器工厂函数:

function color(value: string) { // 这是装饰器工厂
    return function(target) { // 这是装饰器
        // ...
    }
}

装饰器组合

多个装饰器可以同时应用到一个声明上,比如:

  • 书写在同一行上
@f @g x
  • 书写在多行上
@f
@g
x

当多个装饰器应用在同一个声明上的时候,求值的方式和复合函数类似。这个模式下,当复合 f 和 g 的时候,复合的结果是 (f · g),而 (x) 等同于 f(g(x))

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

  • 由上至下以此对装饰器表达式求值
  • 求值的结果会被当做函数,从下至上以此被调用

如果使用装饰器工厂,通过下面的例子观察求值顺序:

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() {}
}

上面代码的编译结果:

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 f() {
    console.log("f() : evaluated");
    return function (target, propertyKey, descriptor) {
        console.log("f(): called");
    };
}
function g() {
    console.log("g(): evaluated");
    return function (target, propertyKey, descriptor) {
        console.log("g(): called");
    };
}
var C = /** @class */ (function () {
    function C() {
    }
    C.prototype.method = function () { };
    __decorate([
        f(),
        g()
    ], C.prototype, "method", null);
    return C;
}());

执行结果:

1.jpg

装饰器求值

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

  • 参数装饰器,然后依次是 方法装饰器访问符装饰器、或 属性装饰器 应用到每个实例成员
  • 参数装饰器,然后依次是 方法装饰器访问符装饰器、或 属性装饰器 应用到每个静态成员
  • 参数装饰器 应用到构造函数
  • 类装饰器 应用到类。

类装饰器

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

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

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

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

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

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

可以像下面这样定义装饰器:

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

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

下面是一个重载构造函数的例子:

function  classDereactor<T extends {new (...args: any[]) : {}}>(constructor: T) {
    return class extends constructor{
        newProperty = 'new property';
        hello = "override";
    }
}

@classDereactor
class Greeter {
    property = "property";
    hello: string;
    constructor (m: string) {
        this.hello = m ;
    }
}

console.log(new Greeter('world'));

编译结果:

var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
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 classDereactor(constructor) {
    return /** @class */ (function (_super) {
        __extends(class_1, _super);
        function class_1() {
            var _this = _super !== null && _super.apply(this, arguments) || this;
            _this.newProperty = 'new property';
            _this.hello = "override";
            return _this;
        }
        return class_1;
    }(constructor));
}
var Greeter = /** @class */ (function () {
    function Greeter(m) {
        this.property = "property";
        this.hello = m;
    }
    Greeter = __decorate([
        classDereactor
    ], Greeter);
    return Greeter;
}());
console.log(new Greeter('world'));

运行结果:

2.jpg

方法装饰器

方法装饰器 声明在一个方法之前(紧靠着方法声明)。它会被应用到方法的 属性描述符 上,可以用来监视、修改或者替换方法定义。

方法装饰器不能用在声明文件(.d.ts),重载或者任何外部上下文(比如 declate 类)中。

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

  1. 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象
  2. 成员的名字
  3. 成员的 属性描述符

注意:如果代码输出目标版本小于 ES5,属性描述符是 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. 成员的 属性描述符

注意:如果代码输出目标版本小于 ES5, property 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(false):

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

属性装饰器

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

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

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。

注意  属性描述符 不会做为参数传入属性装饰器,这与 TypeScrip t是如何初始化属性装饰器的有关。
因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。
返回值也会被忽略。
因此,属性描述符只能用来监视类中是否声明了某个名字的属性。

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

import "reflect-metadata";

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);
    }
}


const formatMetadataKey = '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 库来支持 [实验性的 metadata API](),这个库还不是 ES 标准的一部分,不过当装饰器被 ES 标准采纳后,这些扩展也会被 ES5 标准采纳。

可以通过 npm 安装这个库

yarn add reflect-metadata

TypeScript 支持为带有装饰器的声明生成元数据。

你需要在命令行或 tsconfig.json 里启用 emitDecoratorMetadata编译器选项。

命令行

tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata

tscofnig.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.");
        }
        set(value);
    }
}

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; }
}