参考阅读

简介

装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。在语法上,装饰器有如下几个特征

  • 第一个字符(或者说前缀)是@,后面是一个表达式
  • @后面的表达式,必须是一个函数(或者执行后可以得到一个函数)
  • 这个函数接受所修饰对象的一些相关值作为参数
  • 这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象

举例来说,有一个函数 Injectable() 当作装饰器使用,那么需要写成 @Injectable,然后放在某个类的前面。示例中,由于有了装饰器 @Injectable,类A的行为在运行时就会发生改变

装饰器 - 图1

下面就是一个最简单的装饰器。示例中,函数 simpleDecorator() 用作装饰器,附加在类A之上,后者在代码解析时就会打印一行日志

装饰器 - 图2

编译上面的代码会报错,提示没有用到装饰器的参数。现在就为装饰器加上参数,让它更像正式运行的代码。代码就可以顺利通过编译了,代码含义这里先不解释。大家只要理解,类A在执行前会先执行装饰器simpleDecorator(),并且会向装饰器自动传入参数就可以了

装饰器 - 图3

相比使用子类改变父类,装饰器更加简洁优雅,缺点是不那么直观,功能也受到一些限制。所以,装饰器一般只用来为类添加某种特定行为。示例中,一共有四个装饰器,一个用在类本身(@frozen),另外三个用在类的方法(@configurable、@enumerable、@throttle)。它们不仅增加了代码的可读性,清晰地表达了意图,而且提供一种方便的手段,增加或修改类的功能

装饰器 - 图4

装饰器的版本

TypeScript 从早期开始,就支持装饰器。但是,装饰器的语法后来发生了变化。ECMAScript 标准委员会最终通过的语法标准,与 TypeScript 早期使用的语法有很大差异

  • 目前,TypeScript 5.0 同时支持两种装饰器语法
  • 标准语法可以直接使用,传统语法需要打开 —experimentalDecorators 编译参数

装饰器 - 图5

装饰器的结构

装饰器函数的类型定义如下

装饰器 - 图6

上面代码中,Decorator 是装饰器的类型定义。它是一个函数,使用时会接收到 value 和 context 两个参数

  • value:所装饰的对象
  • context:上下文对象,TypeScript 提供一个原生接口 ClassMethodDecoratorContext,描述这个对象

装饰器 - 图7

装饰器 - 图8

类装饰器

  1. 类装饰器的类型描述如下。类装饰器接受两个参数
    1. value(当前类本身)和 context(上下文对象)
    2. 其中,context 对象的 kind 属性固定为字符串 class

装饰器 - 图9

  1. 类装饰器一般用来对类进行操作,可以不返回任何值,请看下面的例子。示例中,类装饰器 @Greeter 在类 User 的原型对象上,添加了一个 greet() 方法,实例就可以直接使用该方法

装饰器 - 图10

  1. 类装饰器可以返回一个函数,替代当前类的构造方法。示例中,类装饰器@countInstances返回一个函数,替换了类MyClass的构造方法。新的构造方法实现了实例的计数,每新建一个实例,计数器就会加一,并且对实例添加count属性,表示当前实例的编号。注意,下例为了确保新构造方法继承定义在MyClass的原型之上的成员,特别加入A行,确保两者的原型对象是一致的。否则,新的构造函数wrapper的原型对象,与MyClass不同,通不过instanceof运算符

装饰器 - 图11

  1. 类装饰器也可以返回一个新的类,替代原来所装饰的类。示例中,@countInstances 返回一个 MyClass 的子类

装饰器 - 图12

  1. 下面的例子是通过类装饰器,禁止使用new命令新建类的实例。示例中,类装饰器 @functionCallable 返回一个新的构造方法,里面判断 new.target 是否不为空,如果是的,就表示通过new命令调用,从而报错

装饰器 - 图13

  1. 类装饰器的上下文对象 context 的 addInitializer() 方法,用来定义一个类的初始化函数,在类完全定义结束后执行。示例中,类MyComponent定义完成后,会自动执行类装饰器@customElement()给出的初始化函数,该函数会将当前类注册为指定名称(本例为)的自定义 HTML 元素

装饰器 - 图14

方法装饰器

方法装饰器用来装饰类的方法(method)。它的类型描述如下

装饰器 - 图15

根据上面的类型,方法装饰器是一个函数,接受两个参数:value 和 context。参数 value 是方法本身,参数 context 是上下文对象,有以下属性

  • kind:值固定为字符串method,表示当前为方法装饰器
  • name:所装饰的方法名,类型为字符串或 Symbol 值
  • static:布尔值,表示是否为静态方法。该属性为只读属性
  • private:布尔值,表示是否为私有方法。该属性为只读属性
  • access:对象,包含了方法的存取器,但是只有get()方法用来取值,没有set()方法进行赋值
  • addInitializer():为方法增加初始化函数

  1. 方法装饰器会改写类的原始方法,实质等同于下面的操作。示例中,@trace是方法toString()的装饰器,它的效果等同于最后一行对toString()的改写

装饰器 - 图16

  1. 如果方法装饰器返回一个新的函数,就会替代所装饰的原始函数。示例中,装饰器@replaceMethod返回的函数,就成为了新的hello()方法

装饰器 - 图17

示例中,装饰器 @log 的返回值是一个函数 replacementMethod ,替代了原始方法 greet() 。在 replacementMethod() 内部,通过执行 originalMethod.call() 完成了对原始方法的调用

装饰器 - 图18

  1. 利用方法装饰器,可以将类的方法变成延迟执行。示例中,方法装饰器@delay(1000)将方法log()的执行推迟了1秒(1000毫秒)。这里真正的方法装饰器,是delay()执行后返回的函数,delay()的作用是接收参数,用来设置推迟执行的时间。这种通过高阶函数返回装饰器的做法,称为“工厂模式”,即可以像工厂那样生产出一个模子的装饰器

装饰器 - 图19

  1. 方法装饰器的参数context对象里面,有一个addInitializer()方法。它是一个钩子方法,用来在类的初始化阶段,添加回调函数。这个回调函数就是作为addInitializer()的参数传入的,它会在构造方法执行期间执行,早于属性(field)的初始化。下面是addInitializer()方法的一个例子。我们知道,类的方法往往需要在构造方法里面,进行this的绑定。例子中,类Person的构造方法内部,将this与greet()方法进行了绑定。如果没有这一行,将greet()赋值给变量g进行调用,就会报错了

装饰器 - 图20

this的绑定必须放在构造方法里面,因为这必须在类的初始化阶段完成。现在,它可以移到方法装饰器的addInitializer()里面。示例中,绑定this转移到了addInitializer()方法里面

装饰器 - 图21

下面再看一个例子,通过addInitializer()将选定的方法名,放入一个集合。示例中,方法装饰器@collect会将所装饰的成员名字,加入一个 Set 集合collectedMethodKeys

装饰器 - 图22

属性装饰器

属性装饰器用来装饰定义在类顶部的属性(field)。它的类型描述如下,注意,装饰器的第一个参数value的类型是undefined,这意味着这个参数实际上没用的,装饰器不能从value获取所装饰属性的值。另外,第二个参数context对象的kind属性的值为字符串field,而不是“property”或“attribute”,这一点是需要注意的

装饰器 - 图23

  1. 属性装饰器要么不返回值,要么返回一个函数,该函数会自动执行,用来对所装饰属性进行初始化。该函数的参数是所装饰属性的初始值,该函数的返回值是该属性的最终值。示例中,属性装饰器@logged装饰属性name。@logged的返回值是一个函数,该函数用来对属性name进行初始化,它的参数initialValue就是属性name的初始值green。新建实例对象color时,该函数会自动执行

装饰器 - 图24

  1. 属性装饰器的返回值函数,可以用来更改属性的初始值。示例中,属性装饰器@twice返回一个函数,该函数的返回值是属性field的初始值乘以2,所以属性field的最终值是6

装饰器 - 图25

  1. 属性装饰器的上下文对象context的access属性,提供所装饰属性的存取器,请看下面的例子。示例中,access包含了属性name的存取器,可以对该属性进行取值和赋值

装饰器 - 图26

getter | setter 装饰器

注意getter 装饰器的上下文对象context的access属性,只包含get()方法;setter 装饰器的access属性,只包含set()方法。这两个装饰器要么不返回值,要么返回一个函数,取代原来的取值器或存值器

装饰器 - 图27

下面的例子是将取值器的结果,保存为一个属性,加快后面的读取。示例中,第一次读取inst.value,会进行计算,然后装饰器@lazy将结果存入只读属性value,后面再读取这个属性,就不会进行计算了

装饰器 - 图28

accessor 装饰器

装饰器语法引入了一个新的属性修饰符 accessor。示例中,accessor 修饰符等同于为属性x自动生成取值器和存值器,它们作用于私有属性x,也就是说,上面的代码等同于下面的代码

装饰器 - 图29

accessor也可以与静态属性和私有属性一起使用

装饰器 - 图30

accessor 装饰器的类型如下。accessor 装饰器的 value 参数,是一个包含get()方法和set()方法的对象。该装饰器可以不返回值,或者返回一个新的对象,用来取代原来的get()方法和set()方法。此外,装饰器返回的对象还可以包括一个init()方法,用来改变私有属性的初始值

装饰器 - 图31

下面是一个例子。示例中,装饰器@logged为属性x的存值器和取值器,加上了日志输出

装饰器 - 图32

装饰器的执行顺序

装饰器的执行分为两个阶段

  1. 评估(evaluation):计算@符号后面的表达式的值,得到的应该是函数
  2. 应用(application):将评估装饰器后得到的函数,应用于所装饰对象

也就是说,装饰器的执行顺序是,先评估所有装饰器表达式的值,再将其应用于当前类。应用装饰器时

顺序依次为:**方法装饰器、属性装饰器、类装饰器**

装饰器 - 图33

可以看到,类载入的时候,代码按照以下顺序执行

  1. 装饰器评估:这一步计算装饰器的值,首先是类装饰器,然后是类内部的装饰器,按照它们出现的顺序。注意,如果属性名或方法名是计算值(本例是“计算方法名”),则它们在对应的装饰器评估之后,也会进行自身的评估
  2. 装饰器应用:实际执行装饰器函数,将它们与对应的方法和属性进行结合。原型方法的装饰器首先应用,然后是静态属性和静态方法装饰器,接下来是实例属性装饰器,最后是类装饰器。注意“实例属性值”在类初始化的阶段并不执行,直到类实例化时才会执行

如果一个方法或属性有多个装饰器,则内层的装饰器先执行,外层的装饰器后执行。示例中,greet()有两个装饰器,内层的@log先执行,外层的@bound针对得到的结果再执行

装饰器 - 图34

装饰器(旧语法)

https://wangdoc.com/typescript/decorator-legacy