Decorators

装饰

介绍

随着TypeScript和ES6中类的引入,现在存在某些需要额外功能来支持注释或修改类和类成员的场景。装饰器提供了为类声明和成员添加批注和元编程语法的方法。装饰器是JavaScript 的第2阶段提案,可用作TypeScript的实验性功能。

注意装饰器是一个实验性功能,可能会在将来的版本中更改。

要为装饰器启用实验性支持,您必须experimentalDecorators在命令行或以下位置启用编译器选项tsconfig.json

命令行

tsc --target ES5 --experimentalDecorators

tsconfig.json:

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

装饰

装饰是一种特殊种类的声明可被附连到一个类声明,方法,访问器,属性或参数。装饰器使用表单@expression,其中expression必须评估一个函数,该函数将在运行时用关于装饰声明的信息来调用。

例如,给定装饰器,@sealed我们可以sealed按如下方式编写函数:

function sealed(target) { // do something with 'target' ... }

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

装饰工厂

如果我们想定制一个装饰器应用于声明的方式,我们可以编写一个装饰器工厂。甲装饰厂是简单地返回,这将在运行时由装饰被称为表达式的函数。

我们可以用以下方式编写装饰工厂:

function color(value: string) { // this is the decorator factory return function (target) { // this is the decorator // do something with 'target' and 'value'... } }

注意您可以在下面的Method Decorator中看到更详细的装饰工厂示例。

装饰者组成

多个装饰器可以应用于声明,如下面的示例所示:

  • 在一行上:@f @gx

  • 在多行上:

@f @g x

当多个装饰器适用于单个声明时,他们的评估与数学中的函数组合相似。在这个模型中,当构成功能˚F,将得到的复合物(˚F)(X)等效于˚FX))。

因此,在对TypeScript中的单个声明评估多个装饰器时执行以下步骤:

  • 每个装饰器的表达式从上到下进行评估。

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

装饰评估

有一个定义良好的顺序来说明如何应用装饰器应用于类中的各种声明:

  • 参数装饰,接着方法访问器,或物业装饰施加每个实例构件。

2. 参数装饰器,后面跟着MethodAccessorProperty Decorator应用于每个静态成员。

3. 参数装饰器应用于构造函数。

4. 类装饰器适用于该类。

类别装修

类别装饰就是一个类声明之前声明。类装饰器应用于类的构造器,可用于观察,修改或替换类定义。类装饰器不能用于声明文件或任何其他环境上下文中(例如在declare类上)。

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

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

注意如果您选择返回新的构造函数,则必须注意保持原始原型。在运行时应用装饰器的逻辑不会为你做到这一点。

以下是@sealed应用于该类的类decorator()的一个示例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执行,它将封闭构造函数及其原型。

接下来我们有一个如何重写构造函数的例子。

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")

方法装饰器

一个方法装饰只是一个方法声明之前声明。装饰器应用于该方法的属性描述符,并可用于观察,修改或替换方法定义。方法装饰器不能用于声明文件,重载或任何其他环境上下文中(如在declare类中)。

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

  • 无论是静态成员的类的构造函数,还是实例成员的类的原型。

2. 成员的名称。

3. 成员的属性描述符

注意如果您的脚本目标小于ES5,则属性描述符将会变为 undefined

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

注意如果脚本目标小于,则返回值将被忽略ES5

以下是@enumerable应用于Greeter类上方法的decorator()方法的示例:

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属性描述符的属性。

访问器装饰器

访问器装饰器在访问器声明之前声明。访问器装饰器应用于访问器的属性描述符,并可用于观察,修改或替换访问器的定义。访问器装饰器不能用于声明文件或任何其他环境上下文中(例如在declare类中)。

注意TypeScript不允许为单个成员装饰getset访问器。相反,成员的所有装饰器必须应用于以文档顺序指定的第一个访问器。这是因为装饰器适用于属性描述符,该属性描述符组合了访问器getset访问器,而不是单独的每个声明。

访问器装饰器的表达式将在运行时作为函数调用,其中包含以下三个参数:

  • 无论是静态成员的类的构造函数,还是实例成员的类的原型。

2. 成员的名称。

3. 成员的属性描述符

注意如果您的脚本目标小于,则属性描述符将会变为 undefinedES5

如果访问器装饰器返回一个值,它将被用作成员的属性描述符

注意如果脚本目标小于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; } }

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

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

属性装饰器

一个物业装饰只是属性声明之前声明。属性装饰器不能用于声明文件或任何其他环境上下文中(例如在declare类中)。

属性装饰器的表达式将在运行时作为函数调用,其中包含以下两个参数:

  • 无论是静态成员的类的构造函数,还是实例成员的类的原型。

2. 成员的名称。

注意由于属性装饰器在TypeScript中的初始化方式,属性描述符不作为属性装饰器的参数提供。这是因为当定义原型成员时,目前没有机制来描述实例属性,也没有办法观察或修改属性的初始值设定项。返回值也被忽略。因此,属性修饰器只能用于观察为某个类声明了特定名称的属性。

我们可以使用这些信息来记录有关该属性的元数据,如下例所示:

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库的更多信息,请参阅元数据。

参数装饰器

一个参数装饰只是一个参数声明之前声明。参数装饰器应用于类构造函数或方法声明的函数。参数装饰器不能用于声明文件,重载或任何其他环境上下文中(例如在declare类中)。

参数装饰器的表达式将在运行时作为函数调用,其中包含以下三个参数:

  • 无论是静态成员的类的构造函数,还是实例成员的类的原型。

2. 成员的名称。

  • 函数参数列表中参数的序号。

注意参数装饰器只能用于观察已在方法上声明了参数。

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

以下是应用于Greeter类成员参数的参数decorator(@required)的示例:

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为实验性元数据API添加polyfill 的库。这个库还不是ECMAScript(JavaScript)标准的一部分。但是,一旦装饰者被正式采用作为ECMAScript标准的一部分,这些扩展将被提议采用。

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

npm i reflect-metadata --save

TypeScript包含为具有装饰器的声明发出特定类型的元数据的实验性支持。要启用此实验性支持,您必须emitDecoratorMetadata在命令行或您的tsconfig.json以下位置设置编译器选项:

命令行

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

注:装饰元数据是一个实验性功能,可能会在未来版本中引入重大更改。