使用 TypeScript 装饰器装饰你的代码

装饰器让程序员可以编写元信息以内省代码。装饰器的最佳使用场景是横切关注点——面向切面编程。

面向切面编程(AOP)是一种编程范式,它允许我们分离 横切关注点, 藉此达到增加模块化程度的目标。它可以在不修改代码自身的前提下,给已有代码增加额外的行为(通知)。

上面的代码展示了装饰器多么具有声明性。下面我们将介绍装饰器的细节:

  1. 什么是装饰器?它的目的和类型

  2. 装饰器的签名

  3. 方法装饰器

  4. 属性装饰器

  5. 参数装饰器

  6. 访问器装饰器

  7. 类装饰器

  8. 装饰器工厂

  9. 元信息反射 API

  10. 结语

什么是装饰器?它的目的和类型

装饰器是一种特殊的声明,可附加在类、方法、访问器、属性、参数声明上。

装饰器使用 @expression 的形式,其中 expression 必须能够演算为在运行时调用的函数,其中包括装饰声明信息。

它起到了以声明式方法将元信息添加至已有代码的作用。

装饰器类型及其执行优先级为

  1. 类装饰器——优先级 4 (对象实例化,静态)

  2. 方法装饰器——优先级 2 (对象实例化,静态)

  3. 访问器或属性装饰器——优先级 3 (对象实例化,静态)

  4. 参数装饰器——优先级 1 (对象实例化,静态)

注意,如果装饰器应用于类构造函数的参数,那么不同装饰器的优先级为:1. 参数装饰器,2. 方法装饰器,3. 访问器或参数装饰器,4. 构造器参数装饰器,5. 类装饰器。

我们看到,上面的代码中, fg 返回了另一个函数(装饰器函数)。 fg 称为装饰器工厂。

装饰器工厂帮助用户传递可供装饰器利用的参数。

我们还可以看到, 演算顺序由顶向下执行顺序由底向上

装饰器的签名

方法装饰器

从上面的签名中,我们可以看到方法装饰器函数有三个参数:

  1. target—— 当前对象的原型,也就是说,假设 Employee 是对象,那么 target 就是  Employee.prototype

  2. propertyKey—— 方法的名称

  3. descriptor—— 方法的属性描述符,即  Object.getOwnPropertyDescriptor(Employee.prototype,propertyKey)

上面的代码应该算是自解释的——让我们看看编译后的 JavaScript 是什么样的。

让我们开始分析 Employee 函数——构造器初始化 name 参数和 greet 方法,将其加入原型。

这是 TypeScript 自动生成的通用方法,它根据装饰器类型和相应参数处理装饰器函数调用。

该函数有助于内省方法调用,并为开发者铺平了处理类似 日志记忆化应用配置 等横切关注点的道路。

在这个例子中,我们仅仅打印了函数调用及其参数、响应。

注意,阅读 __decorate 方法中的详细注释可以理解其内部机制。

属性装饰器

属性装饰器函数有两个参数:

  1. target—— 当前对象的原型,也就是说,假设 Employee 是对象,那么 target 就是  Employee.prototype

  2. propertyKey—— 属性的名称

上面的代码中,我们在装饰器中内省属性的可访问性。下面是编译后的代码。

参数装饰器

参数装饰器函数有三个参数:

  1. target—— 当前对象的原型,也就是说,假设 Employee 是对象,那么 target 就是  Employee.prototype

  2. propertyKey—— 参数的名称

  3. index—— 参数数组中的位置

在上面的代码中,我们收集了所有被装饰的方法参数的索引或位置,作为元数据加入对象的原型。下面是编译后的代码。

类似之前见过的 __decorate 函数, __param 函数返回一个封装参数装饰器的装饰器。

如我们所见,调用参数装饰器时,会忽略其返回值。这意味着,调用 __param 函数时,其返回值不会用来覆盖参数值。

这就是 参数装饰器不返回 的原因所在。

访问器装饰器

访问器不过是类声明中属性的读取访问器和写入访问器。

访问器装饰器应用于访问器的 属性描述符 ,可用于观测、修改、替换访问器的定义。

上面的例子中,我们定义了两个访问器 namesalary ,并通过装饰器设置是否将其列入清单,据此决定对象的行为。 name 将列入清单,而 salary 不会。

注意:TypeScript 不允许同时装饰单一成员的 getset 访问器。相反,所有成员的装饰器都必须应用于首个指定的访问器(根据文档顺序)。这是因为装饰器应用于属性描述符,属性描述符结合了 getset 访问器,而不是分别应用于每项声明。

下面是编译的代码。

类装饰器

类装饰器应用于类的构造器,可用于观测、修改、替换类定义。

上面的装饰器声明了一个名为 original 的变量,将其值设为被装饰的类构造器。

接着声明了名为 construct 的辅助函数。该函数用于创建类的实例。

我们接下来创建了一个名为 f 的变量,该变量将用作新构造器。该函数调用原构造器,同时在控制台打印实例化的类名。这正是我们 给原构造器加入额外行为 的地方。

原构造器的原型复制到 f ,以确保创建一个 Employee 新实例的时候, instanceof 操作符的效果符合预期。

新构造器一旦就绪,我们便返回它,以完成类构造器的实现。

新构造器就绪之后,每次创建实例时会在控制台打印类名。

编译后的代码如下。

在编译后的代码中,我们注意到两处不同:

  1. 如你所见,传给  __decorate 的参数有两个,装饰器数组和构造器函数。

  2. TypeScript 编译器使用  __decorate 的返回值以覆盖原构造器。

这正是 类装饰器必须返回一个构造函数 的原因所在。

装饰器工厂

由于每种装饰器都有它自身的调用签名,我们可以使用装饰器工厂来泛化装饰器调用。

元信息反射 API

元信息反射 API (例如 Reflect )能够用来以标准方式组织元信息。

「反射」的意思是代码可以侦测同一系统中的其他代码(或其自身)。

反射在组合/依赖注入、运行时类型断言、测试等使用场景下很有用。

上面的代码用到了 reflect-metadata 这个库。其中,我们使用了反射元信息的设计键(例如: design:type )。目前只有三个:

  • 类型元信息用了元信息键  design:type

  • 参数类型元信息用了元信息键  design:paramtypes

  • 返回类型元信息用了元信息键  design:returntype

有了反射,我们就能够在运行时得到以下信息:

  • 实体

  • 实体 类型

  • 实体实现的 接口

  • 实体 构造器参数 的名称和类型。

结语

  • 装饰器不过是在 设计时(design time) 帮助 内省 代码, 注解 及修改类和属性的函数。

  • Yehuda Katz 提议在 ECMAScript 2016 标准中加入装饰器特性: tc39/proposal-decorators

  • 我们可以通过 装饰器工厂 将用户提供的参数传给装饰器。

  • 有 4 种装饰器: 装饰器、 方法 装饰器、 属性/访问器 装饰器、 参数 装饰器。

  • 元信息反射 API有助于以标准方式在对象中加入元信息,以及在 运行时 获取 设计类型信息

我把文中所有代码示例都放到了 mohanramphp/typescript-decorators 这个 Git 仓库中。谢谢阅读!

题图: Alex Loup

其他内容推荐

  • 6 款热门 macOS 开源应用

  • 22 个 iOS 开发热门开源项目

  • 你可能不知道的 npm 实用技巧

本文转载自:New Frontend,原文链接:https://juejin.im/post/5d15e13fe51d45108f254242

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点个「 在看 」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)

  2. 关注我的官网  https:// m uyiy.cn ,让我们成为长期关系

  3. 关注公众号「 高级前端进阶 」,每周重点攻克一个前端面试重难点,公众号后台回复「 资料 」免费送给你精心准备的前端进阶资料。

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章