装饰器

Review

  1. 2024-04-17 07:25
  2. 2024-09-17

[!Summary] 使用场景

  1. 打印日志
  2. 依赖注入
  3. Validation
  4. Caching
  5. Memoization
  6. Event Handling

一、Introduction #

In JavaScript, a decorator is a special kind of function that can modify or extend the behavior of another function, class and their members, accessor, property, or parameter. Decorators provide a way to add metadata or functionality to a target function or class without modifying the source code directly. Decorators use the form @expression .

Decorators are a ==stage 3== proposal in the ECMAScript standard. (2024/04/18)

装饰器分类 #

  1. 函数装饰器
  2. 类装饰器
  3. 成员装饰器
type Decorator = (value: Input, context: {
  kind: string;
  name: string | symbol;
  access: {
    get?(): unknown;
    set?(value: unknown): void;
  };
  private?: boolean;
  static?: boolean;
  addInitializer(initializer: () => void): void;
}) => Output | void;

When decorators are called, they receive two parameters:

  1. The value being decorated, or undefined in the case of class fields which are a special case.
  2. A context object containing information about the value being decorated

The context object also varies depending on the value being decorated. Breaking down the properties:

  • kind: The kind of decorated value. This can be used to assert that the decorator is used correctly, or to have different behavior for different types of values. It is one of the following values.
    • "class"
    • "method"
    • "getter"
    • "setter"
    • "field"
    • "accessor"
  • name: The name of the value, or in the case of private elements the description of it (e.g. the readable name).
  • access: An object containing methods to access the value. These methods also get the final value of the element on the instance, not the current value passed to the decorator. This is important for most use cases involving access, such as type validators or serializers. See the section on Access below for more details.
  • static: Whether or not the value is a static class element. Only applies to class elements.
  • private: Whether or not the value is a private class element. Only applies to class elements.
  • addInitializer: Allows the user to add additional initialization logic to the element or class.
type ClassMethodDecorator = (value: Function, context: {
  kind: "method";
  name: string | symbol;
  access: { get(): unknown };
  static: boolean;
  private: boolean;
  addInitializer(initializer: () => void): void;
}) => Function | void;

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

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

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

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

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;

Decorators are applied after all decorators have been called. The intermediate steps of the decorator application algorithm are not observable–the newly constructed class is not made available until after all method and non-static field decorators have been applied.

The class decorator is called only after all method and field decorators are called and applied.

Finally, static fields are executed and applied.

This decorators proposal uses the syntax of the previous Stage 2 decorators proposal. This means that:

  • Decorator expressions are restricted to a chain of variables, property access with . but not [], and calls (). To use an arbitrary expression as a decorator, @(expression) is an escape hatch.
  • Class expressions may be decorated, not just class declarations.
  • Class decorators may exclusively come before, or after, export/export default.
// @experimentalDecorators
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() {}
}
function Injectable() {
  return function (target: any) {
    target.isInjectable = true;
  };
}

@Injectable()
class MyService {
  // ...
}
function log(func, context) {
  return function (...args) {
    console.log(
      `method: ${func.name} | `,
      `arguments: ${[...args].join(", ")}`
    );

    func.call(this, ...args);
  };
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

new Calculator().add(1, 2); // method: add | arguments: 1, 2

Reference #

  1. TC39 Decorators Proposal
  2. TC39 Decorator Metadata Proposal
  3. TypeScript 5.0 Decorators
  4. A brief history of Decorators in JavaScript