Young Kbt blog Young Kbt blog
首页
  • java基础

    • Java基础
    • Java集合
    • Java反射
    • JavaJUC
    • JavaJVM
  • Java容器

    • JavaWeb
  • Java版本新特性

    • Java新特性
  • SQL 数据库

    • MySQL
    • Oracle
  • NoSQL 数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • ActiveMQ
    • RabbitMQ
    • RocketMQ
    • Kafka
  • 进阶服务

    • Nginx
  • Spring
  • Spring Boot
  • Spring Security
  • 设计模式
  • 算法
  • 知识
  • 管理

    • Maven
    • Git
  • 部署

    • Linux
    • Docker
    • Jenkins
    • Kubernetes
  • 进阶

    • TypeScript
  • 框架

    • React
    • Vue2
    • Vue3
  • 轮子工具
  • 项目工程
  • 友情链接
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 关于
    • Vue2-Admin (opens new window)
    • Vue3-Admin(完善) (opens new window)
GitHub (opens new window)

Shp Liu

朝圣的使徒,正在走向编程的至高殿堂!
首页
  • java基础

    • Java基础
    • Java集合
    • Java反射
    • JavaJUC
    • JavaJVM
  • Java容器

    • JavaWeb
  • Java版本新特性

    • Java新特性
  • SQL 数据库

    • MySQL
    • Oracle
  • NoSQL 数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • ActiveMQ
    • RabbitMQ
    • RocketMQ
    • Kafka
  • 进阶服务

    • Nginx
  • Spring
  • Spring Boot
  • Spring Security
  • 设计模式
  • 算法
  • 知识
  • 管理

    • Maven
    • Git
  • 部署

    • Linux
    • Docker
    • Jenkins
    • Kubernetes
  • 进阶

    • TypeScript
  • 框架

    • React
    • Vue2
    • Vue3
  • 轮子工具
  • 项目工程
  • 友情链接
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 关于
    • Vue2-Admin (opens new window)
    • Vue3-Admin(完善) (opens new window)
GitHub (opens new window)
  • 超文本标记语言 - Html

  • 解释编程语言 - JavaScript

  • JS 超集语言 - TypeScript

    • TypeScript - 介绍
    • TypeScript - 安装和使用
    • TypeScript - 基本类型
    • TypeScript - 编译和配置
    • TypeScript - 文件打包
    • TypeScript - 接口
    • TypeScript - 函数
    • TypeScript - 类
    • TypeScript - 泛型
    • TypeScript - 类型推断
    • TypeScript - 高级类型
    • TypeScript Office - 入门
    • TypeScript Office - 常用类型
    • TypeScript Office - 类型缩小
    • TypeScript Office - 更多函数
    • TypeScript Office - 对象类型
    • TypeScript Office - 类型操纵
    • TypeScript Office - 类
    • TypeScript Office - 模块
    • TypeScript Office - 变量声明
    • TypeScript Office - 类型推断
    • TypeScript Office - 枚举
    • TypeScript Office - 公共类型
    • TypeScript Office - Symbols
    • TypeScript Office - 类型兼容性
    • TypeScript Office - 迭代器和生成器
    • TypeScript Office - 装饰器
      • 简介
      • 装饰器
      • 装饰器工厂
      • 装饰器构成
      • 装饰器评估
      • 类装饰器
      • 方法装饰器
      • 访问器装饰器
      • 属性装饰器
      • 参数装饰器
      • Metadata
    • TypeScript Office - JSX
    • TypeScript Office - 混入
    • TypeScript Office - 三斜线指令
    • TypeScript Office - 模块进阶
    • TypeScript Office - 模块解析
    • TypeScript Office - 命名空间
    • TypeScript Office - 命名空间与模块
    • TypeScript Office - 声明合并
  • 界面构建框架 - React

  • 渐进式框架 - Vue2

  • 渐进式框架 - Vue3

  • 前端
  • JS 超集语言 - TypeScript
Young Kbt
2022-09-15
目录

TypeScript Office - 装饰器

  • 简介
  • 装饰器
  • 装饰器工厂
  • 装饰器构成
  • 装饰器评估
  • 类装饰器
  • 方法装饰器
  • 访问器装饰器
  • 属性装饰器
  • 参数装饰器
  • Metadata

# 简介

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

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

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

  • 命令行
tsc --target ES5 --experimentalDecorators
1
  • tssconfig.json
{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}
1
2
3
4
5
6

# 装饰器

装饰器是一种特殊的声明,可以附加到类声明、方法、访问器、属性或参数上。装饰器使用 @expression 的形式,其中 expression 必须评估为一个函数,该函数将在运行时被调用,并带有关于被装饰的声明的信息。

例如,对于装饰器 @sealed,我们可以将 sealed 的函数写成如下:

function sealed(target) {
    // 对 "target"做一些事情 ...
}
1
2
3

# 装饰器工厂

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

我们可以用以下方式写一个装饰器工厂:

function color(value: string) {
    // 这是装饰器工厂,它设置了
    // 返回的装饰器函数
    return function (target) {
        // 这就是装饰器
        // 用 "target" 和 "value"做一些事情...
    };
}
1
2
3
4
5
6
7
8

# 装饰器构成

多个装饰器可以应用于一个声明,例如在一行中:

@f @g x
1

多行的语法:

@f
@g
x
1
2
3

当多个装饰器适用于一个声明时,它们的评估类似于数学中的函数组合。在这种模式下,当组合函数 f 和 g 时,所产生的组合 (f∘g)(x) 等同于 f(g(x))。

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

  • 每个装饰器的表达式都是自上而下地进行评估的
  • 然后将结果作为函数从下往上调用

如果我们使用装饰器工厂,可以通过下面的例子观察这个评估顺序:

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

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

class ExampleClass {
    @first()
    @second()
    method() {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这将把这个输出打印到控制台:

first(): factory evaluated
second(): factory evaluated
second(): called
first(): called
1
2
3
4

# 装饰器评估

对于应用于类内各种声明的装饰器,有一个明确的顺序:

  • 对于每个实例成员,首先是参数装饰器,然后是方法、访问器或属性装饰器
  • 对于每个静态成员,先是参数装饰器,然后是方法、存取器或属性装饰器
  • 参数装饰器被应用于构造函数
  • 类装饰器适用于类

# 类装饰器

类装饰器就在类声明之前被声明。类装饰器被应用于类的构造函数,可以用来观察、修改或替换类定义。类装饰器不能在声明文件中使用,也不能在任何其他环境下使用(比如在 declare 类上)。

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

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

注意:如果你选择返回一个新的构造函数,必须注意维护原始原型。在运行时应用装饰器的逻辑不会为你这样做。

下面是一个应用于 BugReport 类的类装饰器(@sealed)的例子。

@sealed
class BugReport {
    type = "report";
    title: string;
    constructor(t: string) {
        this.title = t;
    }
}
1
2
3
4
5
6
7
8

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

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}
1
2
3
4

当 @sealed 被执行时,它将同时封闭构造函数和它的原型,因此将阻止在运行时通过访问 BugReport.prototype 或通过定义 BugReport 本身的属性来向该类添加或删除任何进一步的功能(注意 ES2015 类实际上只是基于原型的构造函数的语法糖)。这个装饰器并不能阻止类对 BugReport 进行子类化。

接下来我们有一个如何覆盖构造函数以设置新的默认值的例子:

function reportableClassDecorator<T extends { new (...args: any[]): {} }>
(constructor: T){
    return class extends constructor {
        reportingURL = "http://www...";
    };
}
@reportableClassDecorator
class BugReport {
    type = "report";
    title: string;
    constructor(t: string) {
        this.title = t;
    }
}
const bug = new BugReport("Needs dark mode");
console.log(bug.title); // 打印 "Needs dark mode"
console.log(bug.type); // 打印 "report"
// 注意,装饰器不会改变TypeScript的类型
// 因此,类型系统对新的属性`reportingURL`是不可知的。
bug.reportingURL;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 方法装饰器

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

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

  • 静态成员的类的构造函数,或者实例成员的类的原型
  • 成员的名称
  • 该成员的属性描述符

注意:如果你的脚本目标小于 ES5,属性描述符将无法定义。

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

注意:如果你的脚本目标小于ES5,返回值会被忽略。

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

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}
1
2
3
4
5
6
7
8
9
10

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

function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor:
                      PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}
1
2
3
4
5
6

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

# 访问器装饰器

一个访问器装饰器就在访问器声明之前被声明。访问器装饰器被应用于访问器的属性描述符,可以用来观察、修改或替换访问器的定义。一个访问器装饰器不能在声明文件中使用,也不能在任何其他环境中使用(比如在 declare 类中)。

注意:TypeScript 不允许装饰单个成员的 get 和 set 访问器。相反,该成员的所有装饰器必须应用于文件顺序中指定的第一个访问器。这是因为装饰器适用于一个属性描述符,它结合了获取和设置访问器,而不是每个声明单独。

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

  • 静态成员的类的构造函数,或者实例成员的类的原型
  • 成员的名称
  • 该成员的属性描述符

注意:如果你的脚本目标小于 ES5,属性描述符将无法定义。

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

注意:如果你的脚本目标小于 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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

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

function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor:
                      PropertyDescriptor) {
        descriptor.configurable = value;
    };
}
1
2
3
4
5
6

# 属性装饰器

一个属性装饰器就在一个属性声明之前被声明。一个属性装饰器不能在声明文件中使用,也不能在任何其他环境下使用(比如在 declare 类中)。

属性装饰器的表达式将在运行时作为一个函数被调用,有以下两个参数:

  • 静态成员的类的构造函数,或者实例成员的类的原型
  • 成员的名称

注意:由于属性装饰器在 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);
    }
}
1
2
3
4
5
6
7
8
9
10
11

然后我们可以使用以下函数声明来定义 @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);
}
1
2
3
4
5
6
7
8

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

注意:这个例子需要 reflect-metadata 库。关于 reflect-metadata 库的更多信息,请参见 Metadata (opens new window)。

# 参数装饰器

参数装饰器就在参数声明之前被声明。参数装饰器被应用于类构造器或方法声明的函数。一个参数装饰器不能在声明文件、重载或任何其他环境中使用(比如在 declare 类中)。

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

  • 对于静态成员,可以是该类的构造函数,对于实例成员,可以是该类的原型
  • 该成员的名称
  • 参数在函数的参数列表中的序数索引

注意:一个参数装饰器只能用来观察一个方法上已经声明了一个参数。

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

下面是一个参数装饰器(@required)应用于 BugReport 类的一个成员的参数的例子:

class BugReport {
    type = "report";
    title: string;
    constructor(t: string) {
        this.title = t;
    }
    @validate
    print(@required verbose: boolean) {
        if (verbose) {
            return `type: ${this.type}\ntitle: ${this.title}`;
        } else {
            return this.title;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

然后我们可以使用以下函数声明来定义 @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);
    };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

@required 装饰器添加了一个元数据条目,将参数标记为必填。然后,@validate 装饰器将现有的 greet 方法包装在一个函数中,在调用原始方法之前验证参数。

注意:这个例子需要 reflect-metadata 库。关于 reflect-metadata 库的更多信息,请参见 Metadata (opens new window)。

# Metadata

一些例子使用了 reflect-metadata 库,它为一个实验性的元数据 API。添加了一个 polyfill。这个库还不是 ECMAScript(JavaScript)标准的一部分。然而,一旦装饰器被正式采纳为 ECMAScript 标准的一部分,这些扩展将被提议采纳。

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

npm i reflect-metadata --save
1

TypeScript 包括试验性的支持,为具有装饰器的声明排放某些类型的元数据。要启用这个实验性支持,你必须在命令行或在你的 tsconfig.json 中设置 emitDecoratorMetadata 编译器选项。

  • Command Line
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
1
{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}
1
2
3
4
5
6
7

启用后,只要导入了 reflect-metadata 库,额外的设计时类型信息就会在运行时暴露。

我们可以在下面的例子中看到这个作用:

import "reflect-metadata";
class Point {
    constructor(public x: number, public y: number) {}
}

class Line {
    private _start: Point;
    private _end: Point;
    @validate
    set start(value: Point) {
        this._start = value;
    }
    get start() {
        return this._start;
    }
    @validate
    set end(value: Point) {
        this._end = value;
    }
    get end() {
        return this._end;
    }
}

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, got ${typeof value} not
${type.name}.`);
        }
        set.call(this, value);
    };
}

const line = new Line()
line.start = new Point(0, 0)
// @ts-ignore
// line.end = {}
// 运行时会失败:
// > Invalid type, got object not Point
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

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

class Line {
    private _start: Point;
    private _end: Point;
    @validate
    @Reflect.metadata("design:type", Point)
    set start(value: Point) {
        this._start = value;
    }
    get start() {
        return this._start;
    }
    @validate
    @Reflect.metadata("design:type", Point)
    set end(value: Point) {
        this._end = value;
    }
    get end() {
        return this._end;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

注意:装饰器元数据是一个实验性的功能,在未来的版本中可能会引入破坏性的变化。

编辑此页 (opens new window)
#TypeScript
更新时间: 2023/09/18, 16:34:13
TypeScript Office - 迭代器和生成器
TypeScript Office - JSX

← TypeScript Office - 迭代器和生成器 TypeScript Office - JSX→

最近更新
01
技术随笔 - Element Plus 修改包名 原创
11-02
02
Reactor - 扩展性
11-02
03
Reactor - 最佳实践
11-02
更多文章>
Theme by Vdoing | Copyright © 2021-2024 Young Kbt | blog
桂ICP备2021009994号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式