ts-learnings/19_decorators.md
2019-03-27 11:40:36 +08:00

11 KiB
Raw Blame History

装饰器

Decorators

简介

随着TypeScript及ES6中类的引入就有了一些需要对类与类的成员进行批注与修改等额外特性的场景With the introduction of Classes in TypeScript and ES6, there now exist certain scennarios that require additional features to support annotating or modifying classes and class members。装饰器这一特性就提供了一种将类声明与成员的批注与元编程语法加入进来的方式。装饰器特性是一项JavaScript的第二阶段提议且是TypeScript的一项实验特性。

注意:装饰器是一项实验特性,在以后的版本发布中可能改变。

要开启装饰器的实验性支持,就必须在命令行或tsconfig.json中开启编译器的experimentalDecorators选项:

命令行

tsc --target ES5 --experimentalDecorators

tsconfig.json

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

关于装饰器Decorators

装饰器 是一类特殊的声明,可被附加到类的声明方法访问器属性或者参数。装饰器使用的形式是@expression,其中的expression必须评估为一个将在运行时以有关被装饰声明的信息被调用的函数Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration

比如,对于给定的装饰器@sealed,那么就可能向下面这样写该sealed函数:

function sealed(target) {
    // ... 对`target`进行一些操作 ...
}

注意:在下面的类装饰器中,可以看到更详细的示例

装饰器工厂Decorator Factories

可通过编写一个装饰器工厂,来对装饰器作用于声明的方式进行定制。 装饰器工厂 就是一个返回由装饰器在运行时调用的表达式的函数If you want to customize how a decorator is applied to a declaration, we can write a decorator factory. A Decorator Factory is simply a function that returns the expression that will be called by the decorator at runtime

可以下面的形式,来编写一个装饰器工厂:

function color (value: string) { // 这是装饰器工厂
    return function (target) { // 这是装饰器
        // 以`target`与`value`来完成一些操作
    }
}

注意,在下面的方法装饰器部分,可见到装饰工厂的更详细示例。

装饰器的复合Decorator Composition

对某个声明,可应用多个装饰器,如下面的示例中那样:

  • 在同一行:

    @f @g x
    
  • 在多行上:

    @f
    @g
    x
    

当有多个装饰器应用到单个声明时,它们的执行与数学中的复合函数类似。在这个模型中,当将fg进行复合时,(f∘ g)(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

装饰器求值Decorator Evaluation

对于将装饰器如何应用到类内部的各种声明,有着以下可遵循的定义良好的顺序:

  1. 对于各个实例成员, 参数装饰器,接着分别是 方法访问器 或者 属性装饰器 将被应用( Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each instance member

  2. 对于各个静态成员, 参数装饰器,接着分别是 方法访问器 或者 属性装饰器 将被应用( Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each static member

  3. 对于构造器,将应用参数装饰器( Parameter Decorators are applied for the constructor

  4. 对于类,将应用 类装饰器 Class Decorators are applied for the class )。

类装饰器

类装饰器 是在类声明之前、紧接着类声明处使用的。类声明作用与类的构造器,而可用于对类的定义进行观察、修改或替换。类装饰器不能在声明文件,或任何其它外围上下文中使用(比如在某个declare类上。The class decorator is applied to the constructor of the class and can be used to observe, modify or replace a class definition. A class decorator cannot be used in a declaration file, or in any other ambient context(such as on a declare class))。

什么是TypeScript的外围上下文ambient context, 有的翻译为“已有环境”)? > >

类装饰器的表达式,将被作为一个函数,在运行时以被装饰的类的构造器函数,作为唯一参数而被调用。

注意 应注意返回一个新的构造器函数因为必须注意维护好原来的原型。运行时对装饰器的应用这一逻辑并不会做这件事Should you chose to return a new constructor function, you must take care to maintain the original prototype. The logic that applies decorators at runtime will not do this for you

下面是一个应用到Greeter类的类装饰器(@sealed)的示例:

@sealed
class Greeter {
    greeting: string;

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

    greeter () {
        return `Hello, { this.greeting }`;
    }
}

可将@sealed装饰器定义为使用下面的函数声明:

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

@sealed装饰器(于运行时)被执行后,它将同时封闭构造器及其原型。

接着的是一个如何覆写构造器的示例:

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

@classDecorator
class Greeter {
    property = "property";
    hello: string;

    constructor(m: string) {
        this.hello = m;
    }
}

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

方法装饰器Method Decorators

方法装饰器 是在某个方法声明之前、紧接着该方法处使用的。此种装饰器被应用到方法的 属性描述符 ,而可用于对方法定义进行观察、修改或替换。不能在定义文件、过载或任何其它已有上下文中(比如某个declare类中使用方法装饰器The decorator is applied to the Property Descriptor for the method, and can be used to observe, modify, or replace a method definition. A method decorator cannot be used in a declaration file, on an overload, or in any other ambient context(such as in a declare class))。

方法装饰器的表达式,将在运行时作为函数,以下面的三个参数进行调用:

  1. 静态成员的类构造函数或实例成员的类的原型Either the constructor function of the class for a static member, or the prototype of the class for an instance member, )。

关于静态成员与实例成员的区别:

前面所说的都是对于类的实例成员在实例化后的对象才会起作用。可以使用static定义类中的静态成员所有实例可以使用this中的名称来访问静态成员。 TypeScript笔记

  1. 成员的名称。

  2. 成员的 属性描述符

注意 在低于ES5的目标脚本中 成员描述符 将为 undefined

在方法装饰器有返回值时,其将作为该方法的 属性描述符

注意 在目标脚本低于ES5版本中该返回值将被忽略。

下面是一个应用到Greeter类的方法装饰器(@enumerable)的示例:

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

class Greeter {
    greeting: string;

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

    @enumerable(false)
    greet () {
        return `Hello, ${ this.greeting }`;
    }
}

let g = new Greeter("world");
console.log(g.greet());

这里的@enumerable(false)装饰器是一个装饰器工厂。在@enumerable(false)装饰器被调用时,其就对属性描述符的enumberable属性,进行修改。

访问器装饰器Accessor Decorators

访问器装饰器 是在紧接着某个访问器声明之前进行声明的。访问器装饰器是应用到该访问器的 属性描述符the Property Descriptor 上的,且可用于对某个访问器的定义进行观察、修改或替换。在定义文件、或其他任何外围上下文(比如某个declare的类)中,都不能使用访问器的装饰器。

注意 对与单个成员TypeScript是不允许对其getset访问器进行装饰的。而是该成员的所有装饰器都必须应用到按文档顺序所指定的第一个访问器TypeScript disallows decorating both the get and set accessor for a single member. Instead, all decorators for the member must be applied to the first accessor specified in document order。这是因为应用到 熟悉描述符 的那些结合了getset访问器的装饰器,并不是各自单独声明的。

访问器装饰器的表达式,在运行时将作为函数得以调用,有着以下三个参数:

  1. 对于静态成员,类的构造函数;或对于实例成员,那就就是类的原型

  2. 该成员的名称

  3. 该成员的 属性描述符

在访问器装饰器返回一个值时,该值将作为成员的 属性描述符 得以使用。

注意 在低于ES5的目标脚本下,该返回值将被忽略。

下面是一个应用到Point类的某个成员上的访问器修饰器@configurable的示例:

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