# TypeScript 装饰器

网道（WangDoc.com），互联网文档计划

## 简介 #

装饰器（Decorator）是一种语法结构，用来在定义时修改类（class）的行为。

在语法上，装饰器有如下几个特征。

（1）第一个字符（或者说前缀）是`@`，后面是一个表达式。

（2）`@`后面的表达式，必须是一个函数（或者执行后可以得到一个函数）。

（3）这个函数接受所修饰对象的一些相关值作为参数。

（4）这个函数要么不返回值，要么返回一个新对象取代所修饰的目标对象。

举例来说，有一个函数`Injectable()`当作装饰器使用，那么需要写成`@Injectable`，然后放在某个类的前面。

    @Injectable class A {
      // ...
    }

上面示例中，由于有了装饰器`@Injectable`，类`A`的行为在运行时就会发生改变。

下面就是一个最简单的装饰器。

    function simpleDecorator() {
      console.log('hi');
    }

    @simpleDecorator
    class A {} // "hi"

上面示例中，函数`simpleDecorator()`用作装饰器，附加在类`A`之上，后者在代码解析时就会打印一行日志。

编译上面的代码会报错，提示没有用到装饰器的参数。现在就为装饰器加上参数，让它更像正式运行的代码。

    function simpleDecorator(
      value:any,
      context:any
    ) {
      console.log(`hi, this is ${context.kind} ${context.name}`);
      return value;
    }

    @simpleDecorator
    class A {} // "hi, this is class A"

上面的代码就可以顺利通过编译了，代码含义这里先不解释。大家只要理解，类`A`在执行前会先执行装饰器`simpleDecorator()`，并且会向装饰器自动传入参数就可以了。

装饰器有多种形式，基本上只要在`@`符号后面添加表达式都是可以的。下面都是合法的装饰器。

    @myFunc
    @myFuncFactory(arg1, arg2)

    @libraryModule.prop
    @someObj.method(123)

    @(wrap(dict['prop']))

注意，`@`后面的表达式，最终执行后得到的应该是一个函数。

相比使用子类改变父类，装饰器更加简洁优雅，缺点是不那么直观，功能也受到一些限制。所以，装饰器一般只用来为类添加某种特定行为。

    @frozen class Foo {

      @configurable(false)
      @enumerable(true)
      method() {}

      @throttle(500)
      expensiveMethod() {}
    }

上面示例中，一共有四个装饰器，一个用在类本身（`@frozen`），另外三个用在类的方法（`@configurable`、`@enumerable`、`@throttle`）。它们不仅增加了代码的可读性，清晰地表达了意图，而且提供一种方便的手段，增加或修改类的功能。

## 装饰器的版本 #

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

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

    $ tsc --target ES5 --experimentalDecorators

本章介绍装饰器的标准语法，下一章介绍传统语法。

## 装饰器的结构 #

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

    type Decorator = (
      value: DecoratedValue,
      context: {
        kind: string;
        name: string | symbol;
        addInitializer?(initializer: () => void): void;
        static?: boolean;
        private?: boolean;
        access: {
          get?(): unknown;
          set?(value: unknown): void;
        };
      }
    ) => void | ReplacementValue;

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

- `value`：所装饰的对象。
- `context`：上下文对象，TypeScript 提供一个原生接口`ClassMethodDecoratorContext`，描述这个对象。

    function decorator(
      value:any,
      context:ClassMethodDecoratorContext
    ) {
      // ...
    }

上面是一个装饰器函数，其中第二个参数`context`的类型就可以写成`ClassMethodDecoratorContext`。

`context`对象的属性，根据所装饰对象的不同而不同，其中只有两个属性（`kind`和`name`）是必有的，其他都是可选的。

（1）`kind`：字符串，表示所装饰对象的类型，可能取以下的值。

- 'class'
- 'method'
- 'getter'
- 'setter'
- 'field'
- 'accessor'

这表示一共有六种类型的装饰器。

（2）`name`：字符串或者 Symbol 值，所装饰对象的名字，比如类名、属性名等。

（3）`addInitializer()`：函数，用来添加类的初始化逻辑。以前，这些逻辑通常放在构造函数里面，对方法进行初始化，现在改成以函数形式传入`addInitializer()`方法。注意，`addInitializer()`没有返回值。

（4）`private`：布尔值，表示所装饰的对象是否为类的私有成员。

（5）`static`：布尔值，表示所装饰的对象是否为类的静态成员。

（6）`access`：一个对象，包含了某个值的 get 和 set 方法。

## 类装饰器 #

类装饰器的类型描述如下。

    type ClassDecorator = (
      value: Function,
      context: {
        kind: 'class';
        name: string | undefined;
        addInitializer(initializer: () => void): void;
      }
    ) => Function | void;

类装饰器接受两个参数：`value`（当前类本身）和`context`（上下文对象）。其中，`context`对象的`kind`属性固定为字符串`class`。

类装饰器一般用来对类进行操作，可以不返回任何值，请看下面的例子。

    function Greeter(value, context) {
      if (context.kind === 'class') {
        value.prototype.greet = function () {
          console.log('你好');
        };
      }
    }

    @Greeter
    class User {}

    let u = new User();
    u.greet(); // "你好"

上面示例中，类装饰器`@Greeter`在类`User`的原型对象上，添加了一个`greet()`方法，实例就可以直接使用该方法。

类装饰器可以返回一个函数，替代当前类的构造方法。

    function countInstances(value:any, context:any) {
      let instanceCount = 0;

      const wrapper = function (...args:any[]) {
        instanceCount++;
        const instance = new value(...args);
        instance.count = instanceCount;
        return instance;
      } as unknown as typeof MyClass;

      wrapper.prototype = value.prototype; // A
      return wrapper;
    }

    @countInstances
    class MyClass {}

    const inst1 = new MyClass();
    inst1 instanceof MyClass // true
    inst1.count // 1

上面示例中，类装饰器`@countInstances`返回一个函数，替换了类`MyClass`的构造方法。新的构造方法实现了实例的计数，每新建一个实例，计数器就会加一，并且对实例添加`count`属性，表示当前实例的编号。

注意，上例为了确保新构造方法继承定义在`MyClass`的原型之上的成员，特别加入`A`行，确保两者的原型对象是一致的。否则，新的构造函数`wrapper`的原型对象，与`MyClass`不同，通不过`instanceof`运算符。

类装饰器也可以返回一个新的类，替代原来所装饰的类。

    function countInstances(value:any, context:any) {
      let instanceCount = 0;

      return class extends value {
        constructor(...args:any[]) {
          super(...args);
          instanceCount++;
          this.count = instanceCount;
        }
      };
    }

    @countInstances
    class MyClass {}

    const inst1 = new MyClass();
    inst1 instanceof MyClass // true
    inst1.count // 1

上面示例中，`@countInstances`返回一个`MyClass`的子类。

下面的例子是通过类装饰器，禁止使用`new`命令新建类的实例。

    function functionCallable(
      value:any, {kind}:any
    ):any {
      if (kind === 'class') {
        return function (...args:any) {
          if (new.target !== undefined) {
            throw new TypeError('This function can’t be new-invoked');
          }
          return new value(...args);
        }
      }
    }

    @functionCallable
    class Person {
      name:string;
      constructor(name:string) {
        this.name = name;
      }
    }

    // @ts-ignore
    const robin = Person('Robin');
    robin.name // 'Robin'

上面示例中，类装饰器`@functionCallable`返回一个新的构造方法，里面判断`new.target`是否不为空，如果是的，就表示通过`new`命令调用，从而报错。

类装饰器的上下文对象`context`的`addInitializer()`方法，用来定义一个类的初始化函数，在类完全定义结束后执行。

    function customElement(name: string) {
      return <Input extends new (...args: any) => any>(
        value: Input,
        context: ClassDecoratorContext
      ) => {
        context.addInitializer(function () {
          customElements.define(name, value);
        });
      };
    }

    @customElement("hello-world")
    class MyComponent extends HTMLElement {
      constructor() {
        super();
      }
      connectedCallback() {
        this.innerHTML = `<h1>Hello World</h1>`;
      }
    }

上面示例中，类`MyComponent`定义完成后，会自动执行类装饰器`@customElement()`给出的初始化函数，该函数会将当前类注册为指定名称（本例为`<hello-world>`）的自定义 HTML 元素。

## 方法装饰器 #

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

    type ClassMethodDecorator = (
      value: Function,
      context: {
        kind: 'method';
        name: string | symbol;
        static: boolean;
        private: boolean;
        access: { get: () => unknown };
        addInitializer(initializer: () => void): void;
      }
    ) => Function | void;

根据上面的类型，方法装饰器是一个函数，接受两个参数：`value`和`context`。

参数`value`是方法本身，参数`context`是上下文对象，有以下属性。

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

方法装饰器会改写类的原始方法，实质等同于下面的操作。

    function trace(decoratedMethod) {
      // ...
    }

    class C {
      @trace
      toString() {
        return 'C';
      }
    }

    // `@trace` 等同于
    // C.prototype.toString = trace(C.prototype.toString);

上面示例中，`@trace`是方法`toString()`的装饰器，它的效果等同于最后一行对`toString()`的改写。

如果方法装饰器返回一个新的函数，就会替代所装饰的原始函数。

    function replaceMethod(value:any, context:ClassMethodDecoratorContext) {
      return function (this:Person) {
        return `How are you, ${this.name}?`;
      }
    }

    class Person {
      name: string;

      constructor(name:string) {
        this.name = name;
      }

      @replaceMethod
      hello() {
        return `Hi ${this.name}!`;
      }
    }

    const robin = new Person('Robin');

    robin.hello() // 'How are you, Robin?'

上面示例中，装饰器`@replaceMethod`返回的函数，就成为了新的`hello()`方法。

下面是另一个例子。

    class Person {
      name: string;
      constructor(name: string) {
        this.name = name;
      }

      @log
      greet() {
        console.log(`Hello, my name is ${this.name}.`);
      }
    }

    function log(originalMethod:any, context:ClassMethodDecoratorContext) {
      const methodName = String(context.name);

      function replacementMethod(this: any, ...args: any[]) {
        console.log(`LOG: Entering method '${methodName}'.`)
        const result = originalMethod.call(this, ...args);
        console.log(`LOG: Exiting method '${methodName}'.`)
        return result;
      }

      return replacementMethod;
    }

    const person = new Person('张三');
    person.greet()
    // "LOG: Entering method 'greet'."
    // "Hello, my name is 张三."
    // "LOG: Exiting method 'greet'."

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

利用方法装饰器，可以将类的方法变成延迟执行。

    function delay(milliseconds: number = 0) {
      return function (value, context) {
        if (context.kind === "method") {
          return function (...args: any[]) {
            setTimeout(() => {
              value.apply(this, args);
            }, milliseconds);
          };
        }
      };
    }

    class Logger {
      @delay(1000)
      log(msg: string) {
        console.log(`${msg}`);
      }
    }

    let logger = new Logger();
    logger.log("Hello World");

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

方法装饰器的参数`context`对象里面，有一个`addInitializer()`方法。它是一个钩子方法，用来在类的初始化阶段，添加回调函数。这个回调函数就是作为`addInitializer()`的参数传入的，它会在构造方法执行期间执行，早于属性（field）的初始化。

下面是`addInitializer()`方法的一个例子。我们知道，类的方法往往需要在构造方法里面，进行`this`的绑定。

    class Person {
      name: string;
      constructor(name: string) {
        this.name = name;

        // greet() 绑定 this
        this.greet = this.greet.bind(this);
      }

      greet() {
        console.log(`Hello, my name is ${this.name}.`);
      }
    }

    const g = new Person('张三').greet;
    g() // "Hello, my name is 张三."

上面例子中，类`Person`的构造方法内部，将`this`与`greet()`方法进行了绑定。如果没有这一行，将`greet()`赋值给变量`g`进行调用，就会报错了。

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

    function bound(
      originalMethod:any, context:ClassMethodDecoratorContext
    ) {
      const methodName = context.name;
      if (context.private) {
        throw new Error(`不能绑定私有方法 ${methodName as string}`);
      }
      context.addInitializer(function () {
        this[methodName] = this[methodName].bind(this);
      });
    }

上面示例中，绑定`this`转移到了`addInitializer()`方法里面。

下面再看一个例子，通过`addInitializer()`将选定的方法名，放入一个集合。

    function collect(
      value,
      {name, addInitializer}
    ) {
      addInitializer(function () {
        if (!this.collectedMethodKeys) {
          this.collectedMethodKeys = new Set();
        }
        this.collectedMethodKeys.add(name);
      });
    }

    class C {
      @collect
      toString() {}

      @collect
      [Symbol.iterator]() {}
    }

    const inst = new C();
    inst.collectedMethodKeys // new Set(['toString', Symbol.iterator])

上面示例中，方法装饰器`@collect`会将所装饰的成员名字，加入一个 Set 集合`collectedMethodKeys`。

## 属性装饰器 #

属性装饰器用来装饰定义在类顶部的属性（field）。它的类型描述如下。

    type ClassFieldDecorator = (
      value: undefined,
      context: {
        kind: 'field';
        name: string | symbol;
        static: boolean;
        private: boolean;
        access: { get: () => unknown, set: (value: unknown) => void };
        addInitializer(initializer: () => void): void;
      }
    ) => (initialValue: unknown) => unknown | void;

注意，装饰器的第一个参数`value`的类型是`undefined`，这意味着这个参数实际上没用的，装饰器不能从`value`获取所装饰属性的值。另外，第二个参数`context`对象的`kind`属性的值为字符串`field`，而不是“property”或“attribute”，这一点是需要注意的。

属性装饰器要么不返回值，要么返回一个函数，该函数会自动执行，用来对所装饰属性进行初始化。该函数的参数是所装饰属性的初始值，该函数的返回值是该属性的最终值。

    function logged(value, context) {
      const { kind, name } = context;
      if (kind === 'field') {
        return function (initialValue) {
          console.log(`initializing ${name} with value ${initialValue}`);
          return initialValue;
        };
      }
    }

    class Color {
      @logged name = 'green';
    }

    const color = new Color();
    // "initializing name with value green"

上面示例中，属性装饰器`@logged`装饰属性`name`。`@logged`的返回值是一个函数，该函数用来对属性`name`进行初始化，它的参数`initialValue`就是属性`name`的初始值`green`。新建实例对象`color`时，该函数会自动执行。

属性装饰器的返回值函数，可以用来更改属性的初始值。

    function twice() {
      return initialValue => initialValue * 2;
    }

    class C {
      @twice
      field = 3;
    }

    const inst = new C();
    inst.field // 6

上面示例中，属性装饰器`@twice`返回一个函数，该函数的返回值是属性`field`的初始值乘以2，所以属性`field`的最终值是6。

属性装饰器的上下文对象`context`的`access`属性，提供所装饰属性的存取器，请看下面的例子。

    let acc;

    function exposeAccess(
      value, {access}
    ) {
      acc = access;
    }

    class Color {
      @exposeAccess
      name = 'green'
    }

    const green = new Color();
    green.name // 'green'

    acc.get(green) // 'green'

    acc.set(green, 'red');
    green.name // 'red'

上面示例中，`access`包含了属性`name`的存取器，可以对该属性进行取值和赋值。

## getter 装饰器，setter 装饰器 #

getter 装饰器和 setter 装饰器，是分别针对类的取值器（getter）和存值器（setter）的装饰器。它们的类型描述如下。

    type ClassGetterDecorator = (
      value: Function,
      context: {
        kind: 'getter';
        name: string | symbol;
        static: boolean;
        private: boolean;
        access: { get: () => unknown };
        addInitializer(initializer: () => void): void;
      }
    ) => Function | void;

    type ClassSetterDecorator = (
      value: Function,
      context: {
        kind: 'setter';
        name: string | symbol;
        static: boolean;
        private: boolean;
        access: { set: (value: unknown) => void };
        addInitializer(initializer: () => void): void;
      }
    ) => Function | void;

注意，getter 装饰器的上下文对象`context`的`access`属性，只包含`get()`方法；setter 装饰器的`access`属性，只包含`set()`方法。

这两个装饰器要么不返回值，要么返回一个函数，取代原来的取值器或存值器。

下面的例子是将取值器的结果，保存为一个属性，加快后面的读取。

    class C {
      @lazy
      get value() {
        console.log('正在计算……');
        return '开销大的计算结果';
      }
    }

    function lazy(
      value:any,
      {kind, name}:any
    ) {
      if (kind === 'getter') {
        return function (this:any) {
          const result = value.call(this);
          Object.defineProperty(
            this, name,
            {
              value: result,
              writable: false,
            }
          );
          return result;
        };
      }
      return;
    }

    const inst = new C();
    inst.value
    // 正在计算……
    // '开销大的计算结果'
    inst.value
    // '开销大的计算结果'

上面示例中，第一次读取`inst.value`，会进行计算，然后装饰器`@lazy`将结果存入只读属性`value`，后面再读取这个属性，就不会进行计算了。

## accessor 装饰器 #

装饰器语法引入了一个新的属性修饰符`accessor`。

    class C {
      accessor x = 1;
    }

上面示例中，`accessor`修饰符等同于为公开属性`x`自动生成取值器和存值器，它们作用于私有属性`x`。（注意，公开的`x`与私有的`x`不是同一个属性。）也就是说，上面的代码等同于下面的代码。

    class C {
      #x = 1;

      get x() {
        return this.#x;
      }

      set x(val) {
        this.#x = val;
      }
    }

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

    class C {
      static accessor x = 1;
      accessor #y = 2;
    }

accessor 装饰器的类型如下。

    type ClassAutoAccessorDecorator = (
      value: {
        get: () => unknown;
        set: (value: unknown) => void;
      },
      context: {
        kind: "accessor";
        name: string | symbol;
        access: { get(): unknown, set(value: unknown): void };
        static: boolean;
        private: boolean;
        addInitializer(initializer: () => void): void;
      }
    ) => {
      get?: () => unknown;
      set?: (value: unknown) => void;
      init?: (initialValue: unknown) => unknown;
    } | void;

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

下面是一个例子。

    class C {
      @logged accessor x = 1;
    }

    function logged(value, { kind, name }) {
      if (kind === "accessor") {
        let { get, set } = value;

        return {
          get() {
            console.log(`getting ${name}`);

            return get.call(this);
          },

          set(val) {
            console.log(`setting ${name} to ${val}`);

            return set.call(this, val);
          },

          init(initialValue) {
            console.log(`initializing ${name} with value ${initialValue}`);
            return initialValue;
          }
        };
      }
    }

    let c = new C();

    c.x;
    // getting x

    c.x = 123;
    // setting x to 123

上面示例中，装饰器`@logged`为属性`x`的存值器和取值器，加上了日志输出。

## 装饰器的执行顺序 #

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

（1）评估（evaluation）：计算`@`符号后面的表达式的值，得到的应该是函数。

（2）应用（application）：将评估装饰器后得到的函数，应用于所装饰对象。

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

应用装饰器时，顺序依次为方法装饰器和属性装饰器，然后是类装饰器。

请看下面的例子。

    function d(str:string) {
      console.log(`评估 @d(): ${str}`);
      return (
        value:any, context:any
      ) => console.log(`应用 @d(): ${str}`);
    }

    function log(str:string) {
      console.log(str);
      return str;
    }

    @d('类装饰器')
    class T {
      @d('静态属性装饰器')
      static staticField = log('静态属性值');

      @d('原型方法')
      [log('计算方法名')]() {}

      @d('实例属性')
      instanceField = log('实例属性值');

      @d('静态方法装饰器')
      static fn(){}
    }

上面示例中，类`T`有四种装饰器：类装饰器、静态属性装饰器、方法装饰器、属性装饰器。

它的运行结果如下。

    评估 @d(): 类装饰器
    评估 @d(): 静态属性装饰器
    评估 @d(): 原型方法
    计算方法名
    评估 @d(): 实例属性
    评估 @d(): 静态方法装饰器
    应用 @d(): 静态方法装饰器
    应用 @d(): 原型方法
    应用 @d(): 静态属性装饰器
    应用 @d(): 实例属性
    应用 @d(): 类装饰器
    静态属性值

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

（1）装饰器评估：这一步计算装饰器的值，首先是类装饰器，然后是类内部的装饰器，按照它们出现的顺序。

注意，如果属性名或方法名是计算值（本例是“计算方法名”），则它们在对应的装饰器评估之后，也会进行自身的评估。

（2）装饰器应用：实际执行装饰器函数，将它们与对应的方法和属性进行结合。

静态方法装饰器首先应用，然后是原型方法的装饰器和静态属性装饰器，接下来是实例属性装饰器，最后是类装饰器。

注意，“实例属性值”在类初始化的阶段并不执行，直到类实例化时才会执行。

如果一个方法或属性有多个装饰器，则内层的装饰器先执行，外层的装饰器后执行。

    class Person {
      name: string;
      constructor(name: string) {
        this.name = name;
      }

      @bound
      @log
      greet() {
        console.log(`Hello, my name is ${this.name}.`);
      }
    }

上面示例中，`greet()`有两个装饰器，内层的`@log`先执行，外层的`@bound`针对得到的结果再执行。

## 参考链接 #

- JavaScript metaprogramming with the 2022-03 decorators API
- TS 5.0 Beta: New Decorators Are Here!, Bytefer

本教程采用知识共享 署名-相同方式共享 3.0协议。

分享本文