ts-learnings/19_decorators.md

496 lines
19 KiB
Markdown
Raw Normal View History

2019-03-27 11:40:36 +08:00
# 装饰器
**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的[第二阶段提议](https://github.com/tc39/proposal-decorators)且是TypeScript的一项实验特性。
> 注意:装饰器是一项实验特性,在以后的版本发布中可能改变。
要开启装饰器的实验性支持,就必须在命令行或`tsconfig.json`中开启编译器的`experimentalDecorators`选项:
**命令行**
```bash
tsc --target ES5 --experimentalDecorators
```
**tsconfig.json**
```json
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
```
## 关于装饰器Decorators
2019-03-27 16:51:18 +08:00
*装饰器* 是一类特殊的声明,可被附加到[类的声明](#class-decorators)、[方法](#method-decorators)、[访问器](#accessor-decorators)、[属性](#property-decorators)或者[参数](#parameter-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
2019-03-27 11:40:36 +08:00
比如,对于给定的装饰器`@sealed`,那么就可能向下面这样写该`sealed`函数:
```typescript
function sealed(target) {
// ... 对`target`进行一些操作 ...
}
```
> 注意:在下面的[类装饰器](#class-decorators)中,可以看到更详细的示例
2019-03-27 16:53:34 +08:00
<a name="decorator-factories"></a>
2019-03-28 17:15:58 +08:00
## 装饰器工厂Decorator Factories
2019-03-27 11:40:36 +08:00
可通过编写一个装饰器工厂,来对装饰器作用于声明的方式进行定制。 *装饰器工厂* 就是一个返回由装饰器在运行时调用的表达式的函数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
可以下面的形式,来编写一个装饰器工厂:
```typescript
function color (value: string) { // 这是装饰器工厂
return function (target) { // 这是装饰器
// 以`target`与`value`来完成一些操作
}
}
```
> 注意,在下面的[方法装饰器](#method-decorators)部分,可见到装饰工厂的更详细示例。
2019-03-28 17:15:58 +08:00
## 装饰器的复合Decorator Composition
2019-03-27 11:40:36 +08:00
对某个声明,可应用多个装饰器,如下面的示例中那样:
+ 在同一行:
```typescript
@f @g x
```
+ 在多行上:
```typescript
@f
@g
x
```
当有多个装饰器应用到单个声明时,它们的执行与[数学中的复合函数](http://en.wikipedia.org/wiki/Function_composition)类似。在这个模型中,当将`f`与`g`进行复合时,`(f∘ g)(x)`复合结果与`f(g(x))`等价。
因此TypeScript中在对单一声明上的多个装饰器进行执行时将完成以下步骤
1. 各个装饰器的表达式将自顶向下执行。
2. 随后的结果作为函数被自底向上进行调用。
当使用了[装饰器工厂](#decorator-factories),就可以在下面的示例中观察到这种执行顺序:
```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() {}
}
```
其将把下面的输出打印到控制台:
```bash
f(): evaluated
g(): evaluated
g(): called
f(): called
```
2019-03-28 17:15:58 +08:00
## 装饰器求值Decorator Evaluation
2019-03-27 11:40:36 +08:00
对于将装饰器如何应用到类内部的各种声明,有着以下可遵循的定义良好的顺序:
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 )。
2019-03-27 17:43:05 +08:00
<a name="class-decorators"></a>
2019-03-28 17:15:58 +08:00
## 类装饰器
2019-03-27 11:40:36 +08:00
*类装饰器* 是在类声明之前、紧接着类声明处使用的。类声明作用与类的构造器,而可用于对类的定义进行观察、修改或替换。类装饰器不能在声明文件,或任何其它外围上下文中使用(比如在某个`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`)的示例:
```typescript
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greeter () {
return `Hello, { this.greeting }`;
}
}
```
可将`@sealed`装饰器定义为使用下面的函数声明:
```typescript
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
```
在`@sealed`装饰器(于运行时)被执行后,它将同时封闭构造器及其原型。
接着的是一个如何覆写构造器的示例:
```typescript
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"));
```
2019-03-27 17:43:05 +08:00
<a name="method-decorators"></a>
2019-03-28 17:15:58 +08:00
## 方法装饰器Method Decorators
2019-03-27 11:40:36 +08:00
*方法装饰器* 是在某个方法声明之前、紧接着该方法处使用的。此种装饰器被应用到方法的 *属性描述符* ,而可用于对方法定义进行观察、修改或替换。不能在定义文件、过载或任何其它已有上下文中(比如某个`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笔记](http://yrq110.me/2018/01/06/20180106-typescript-note/)
2. 成员的名称。
3. 成员的 *属性描述符*
> **注意** 在低于ES5的目标脚本中 *成员描述符* 将为 `undefined`。
在方法装饰器有返回值时,其将作为该方法的 *属性描述符*
> **注意** 在目标脚本低于ES5版本中该返回值将被忽略。
下面是一个应用到`Greeter`类的方法装饰器(`@enumerable`)的示例:
```typescript
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`属性,进行修改。
2019-03-27 17:43:05 +08:00
<a name="accessor-decorators"></a>
2019-03-28 17:15:58 +08:00
## 访问器装饰器Accessor Decorators
2019-03-27 11:40:36 +08:00
*访问器装饰器* 是在紧接着某个访问器声明之前进行声明的。访问器装饰器是应用到该访问器的 *属性描述符the Property Descriptor* 上的,且可用于对某个访问器的定义进行观察、修改或替换。在定义文件、或其他任何外围上下文(比如某个`declare`的类)中,都不能使用访问器的装饰器。
> **注意** 对与单个成员TypeScript是不允许对其`get`或`set`访问器进行装饰的。而是该成员的所有装饰器都必须应用到按文档顺序所指定的第一个访问器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。这是因为应用到 *熟悉描述符* 的那些结合了`get`与`set`访问器的装饰器,并不是各自单独声明的。
访问器装饰器的表达式,在运行时将作为函数得以调用,有着以下三个参数:
1. 对于静态成员,类的构造函数;或对于实例成员,那就就是类的原型
2. 该成员的名称
3. 该成员的 *属性描述符*
在访问器装饰器返回一个值时,该值将作为成员的 *属性描述符* 得以使用。
> **注意** 在低于`ES5`的目标脚本下,该返回值将被忽略。
下面是一个应用到`Point`类的某个成员上的访问器修饰器`@configurable`的示例:
```typescript
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; }
}
```
2019-03-27 16:51:18 +08:00
使用以下的函数声明,可定义出该`@configurable`装饰器:
```typescript
function configurable (value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
}
}
```
2019-03-27 17:43:05 +08:00
<a name="property-decorators"></a>
2019-03-28 17:15:58 +08:00
## 属性装饰器Property Decorators
2019-03-27 16:51:18 +08:00
*属性装饰器* 是紧接着某个属性声明之前进行声明的。在声明文件中,以及任何其他外围上下文(比如在某个`declare`类中),都不能使用属性装饰器。
属性装饰器的表达式,将在运行时作为函数进行调用,有着以下两个参数:
1. 某个静态成员的类构造函数,或某个实例成员的类的原型;
2. 该成员的名称。
> **注意** 由于在TypeScript中属性装饰器初始化方式的原因将不会把 *属性描述符* 提供给属性装饰器。这是因为在定义某个原型的成员时,目前还没有对实例属性进行描述的机制,同时也没有对某个属性的初始化器进行观察与修改的途径。由于上述原因,属性装饰器的返回值也被加以忽略了。那么属性装饰器就只能用于已声明为某个类的、某个指定名称的属性进行观察了。
有了这些信息,就可以记录有关该属性的元数据了,如下面的示例:
```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`函数:
```typescript
import "reflect-metadata";
// const formatMetadataKey = Symbol("format");
// const formatMetadataKey: Symbol;
//
// 上面两种写法都不行估计是新版本的typescript已经不支持 Symbol 类型变量的初始化了
let formatMetadataKey: Symbol;
function format (formatMetadataKey: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat (target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
```
2019-03-27 17:43:05 +08:00
这里的装饰器 `@format("Hello, %s")`是一个 [装饰器工厂](#decorator-factories) 。在调用`@format("Hello, %s")`时,该函数就使用`reflect-metadata`库`Reflect.metadata`函数,添加该属性`greeting`的一个元数据条目。在调用`getFormat`时,`getFormat`函数就读取到那个格式的元数据值了。
> **注意** 此示例需要`reflect-metadata`库。请参阅 [元数据](#metadata) 部分了解有关 `reflect-metadata`库更多的信息。
<a name="parameter-decorators"></a>
2019-03-28 17:15:58 +08:00
## 参数装饰器parameter decorators
2019-03-27 17:43:05 +08:00
*参数装饰器*是紧接着某个参数声明之前进行声明的。参数装饰器应用到类构造器或类的方法声明的函数上的the parameter decorator is applied to the function for a class constructor or method declaration。参数装饰器不能用在声明文件`.d.ts`、重载overload或其他外围上下文ambient context比如在某个`declare`类中)。
参数装饰器的表达式在运行时将作为函数加以调用,其有着以下三个参数:
1. 静态成员的类的构造函数,或实例成员的类的原型;
2. 成员的名称;
3. 对应参数在函数参数列表中的顺序索引。
> **注意** 参数装饰器只能用于对某个方法上已声明的某个参数进行观察A parameter decorator can only be used to observe that a parameter has been declared on a method
以下是一个参数装饰器(`@required`)的示例,该参数装饰器应用到`Greeter`类的成员的参数上:
```typescript
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}
```
可使用下面的函数声明,来定义出`@required`与`@validate`两个装饰器:
```typescript
import "reflect-metadata";
let requiredMetadataKey: Symbol;
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("缺少需要的参数。");
}
}
}
return method.apply(this, arguments);
}
}
```
2019-03-28 17:15:58 +08:00
这里的`@required`装饰器加入了一个将该参数标识为必需的元数据条目the `@required` decorator adds a metadata entry that marks the parameter as required。而`@validate`装饰器随后将现有的`greet`方法,封装为一个在调用该原始方法前,对参数进行验证的函数。
> **注意** 此示例需要`reflect-metadata`库。请参阅 [元数据](#metadata) 了解更多有个该`reflect-metadata`库的信息。
<a name="metadata"></a>
## 关于元数据Metadata
上面的示例使用到加入了 [实验性元数据 API](https://github.com/rbuckton/ReflectDecorators)的 `reflect-metadata`库。该库还没有成为 ECMASriptJavaScript标准的一部分。但如果装饰器一旦作为ECMAScript标准而正式采纳那么这些扩展也将被提议采用。
可通过`npm`来安装该库:
```sh
npm i reflect-metadata --save-dev
```
TypeScript包含了对那些有着装饰器的声明的确定类型的元数据的生成的实验性支持TypeScript includes experimental support for emitting certain types of metadata for declarations that have decorators。要开启此项实验性支持功能就必须通过命令行或在`tsconfig.json`文件中对`emitDecoratorMetadata`编译器选项进行设置:
**命令行方式**
```sh
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
```
**`tsconfig.json方式`**
```json
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
```
在开启了这些选项,同时导入了`reflect-metadata`库之后,那些设计阶段的额外类型信息,将在运行时得以暴露。
在以下示例中,可观察到这一点起了作用:
```typescript
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("无效的类型。");
}
set(value);
}
}
```
> **注意** 装饰器元数据是一项实验性特性TypeScript以后的发布可能引入对此的剧烈变动。