Skip to content

装饰器stage3

✍️ w 🕒 2024-06-22 13:10:29(a minute ago) 🔗 A.前端知识整理

装饰器的发展历程

  • 2014-04-10: Yehuda Katz 向 TC39 提出装饰器提案,进入 Stage 0。
  • 2015-03-05: Angular 团队和 TypeScript 团队宣布 Angular 将从 AtScript 切换到 TypeScript,并采用装饰器。
  • 2015-03-24: 装饰器提案达到 Stage 1。
  • 2016-07-28: 提案达到 Stage 2。
  • 2022-03-28: 装饰器提案达到 Stage 3。

装饰器有两种主要的提案:第二提案(Stage 2)和第三提案(Stage 3)。二者虽然都旨在增强代码的可读性和可维护性,但在语法和功能上存在一些差异,在新版本的ts5 可以直接使用装饰器但是默认 stage3 提案的

新的装饰器不支持装饰参数TypeScript 5+装饰器变更的影响

另一个改动在装饰器函数定义上,之前装饰函数定义入参可以理解 target key descriptor, 并且在第二提案的时候 使用描述符对象(descriptor),需要显式操作描述符,但是在提案 3 使用上下文对象(context),提供更详细和结构化的元数据。

看一下新版的装饰器函数定义

好的,我会将这些参数解释为一个表格:

参数名称类型说明
valueDecoratedValue被装饰的值。
contextobject上下文对象,包含装饰器的上下文信息。
context.kindstring装饰器应用的类型,例如属性、方法、访问器等。
context.name`stringsymbol`
context.addInitializerfunction可选函数,用于添加初始化逻辑。
context.staticboolean可选属性,指示是否是静态成员。
context.privateboolean可选属性,指示是否是私有成员。
context.accessobject访问方法对象,包含获取和设置方法。
context.access.getfunction可选属性,返回值的获取方法。
context.access.setfunction可选属性,设置值的方法。
返回值`voidReplacementValue`

解释部分:

  • value:这是被装饰的原始值,类型因装饰器作用的目标不同而异。
  • context:这是一个包含上下文信息的对象,用于提供装饰器的环境信息。
  • context.kind:标识装饰器应用的对象类型,例如class、method、getter、setter、field、accessor。
  • context.name:被装饰的对象的名称,可以是字符串或符号。
  • context.addInitializer:一个可选的函数,以前,这些逻辑通常放在构造函数里面,对方法进行初始化,现在改成以函数形式传入addInitializer()方法。注意,addInitializer()没有返回值。可以用于添加初始化逻辑,该函数在装饰器应用后会被调用。
  • context.static:一个可选布尔值,指示被装饰的对象是否是静态成员。
  • context.private:一个可选布尔值,指示被装饰的对象是否是私有成员。
  • context.access:一个对象,包含访问方法,用于获取和设置被装饰对象的值。
  • context.access.get:一个可选的函数,用于获取被装饰对象的值。
  • context.access.set:一个可选的函数,用于设置被装饰对象的值。
  • 返回值:装饰器的返回值,可以是 void 或者是一个替换值,如果返回替换值,它将替代原始值。
ts
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;
  1. ClassDecoratorContext
属性类型说明
kind"class"被装饰的元素类型,类装饰器上下文中总是 "class"
namestring | undefined被装饰的类的名称
addInitializer(initializer: (this: Class) => void) => void添加类定义完成后的回调函数
metadataDecoratorMetadata装饰器元数据
  1. ClassMethodDecoratorContext
属性类型说明
kind"method"被装饰的元素类型,方法装饰器上下文中总是 "method"
namestring | symbol被装饰的类元素的名称
staticboolean表示类元素是静态(true)还是实例(false)
privateboolean表示类元素名字是否是私有的
accessObject可以用于在运行时访问类元素当前值的对象,上下文会有所不同
access.has(object: This) => boolean检查对象是否有与装饰的元素相同名称的属性
access.get(object: This) => Value从提供的对象获取当前方法的值
addInitializer(initializer: (this: This) => void) => void添加回调函数,在静态初始化器或实例初始化器运行前调用
metadataDecoratorMetadata装饰器元数据
  1. ClassGetterDecoratorContext
属性类型说明
kind"getter"被装饰的元素类型,getter装饰器上下文中总是 "getter"
namestring | symbol被装饰的类元素的名称
staticboolean表示类元素是静态(true)还是实例(false)
privateboolean表示类元素名字是否是私有的
accessObject可以用于在运行时访问类元素当前值的对象,上下文会有所不同
access.has(object: This) => boolean检查对象是否有与装饰的元素相同名称的属性
access.get(object: This) => Value调用getter方法从提供的对象上获取值
addInitializer(initializer: (this: This) => void) => void添加回调函数,在静态初始化器或实例初始化器运行前调用
metadataDecoratorMetadata装饰器元数据
  1. ClassSetterDecoratorContext
属性类型说明
kind"setter"被装饰的元素类型,setter装饰器上下文中总是 "setter"
namestring | symbol被装饰的类元素的名称
staticboolean表示类元素是静态(true)还是实例(false)
privateboolean表示类元素名字是否是私有的
accessObject可以用于在运行时访问类元素当前值的对象,上下文会有所不同
access.has(object: This) => boolean检查对象是否有与装饰的元素相同名称的属性
access.set(object: This, value: Value) => void调用setter从提供的对象上设置值
addInitializer(initializer: (this: This) => void) => void添加回调函数,在静态初始化器或实例初始化器运行前调用
metadataDecoratorMetadata装饰器元数据
  1. ClassAccessorDecoratorContext
属性类型说明
kind"accessor"被装饰的元素类型,accessor装饰器上下文中总是 "accessor"
namestring | symbol被装饰的类元素的名称
staticboolean表示类元素是静态(true)还是实例(false)
privateboolean表示类元素名字是否是私有的
accessObject可以用于在运行时访问类元素当前值的对象,上下文会有所不同
access.has(object: This) => boolean检查对象是否有与装饰的元素相同名称的属性
access.get(object: This) => Value调用getter方法从提供的对象上获取值
access.set(object: This, value: Value) => void调用setter从提供的对象上设置值
addInitializer(initializer: (this: This) => void) => void添加回调函数,在静态初始化器或实例初始化器运行前调用
metadataDecoratorMetadata装饰器元数据
  1. ClassFieldDecoratorContext
属性类型说明
kind"field"被装饰的元素类型,字段装饰器上下文中总是 "field"
namestring | symbol被装饰的类元素的名称
staticboolean表示类元素是静态(true)还是实例(false)
privateboolean表示类元素名字是否是私有的
accessObject可以用于在运行时访问类元素当前值的对象,上下文会有所不同
access.has(object: This) => boolean检查对象是否有与装饰的元素相同名称的属性
access.get(object: This) => Value从提供的对象获取字段的值
access.set(object: This, value: Value) => void设置提供的对象的字段
addInitializer(initializer: (this: This) => void) => void添加回调函数,在静态初始化器或实例初始化器运行前调用
metadataDecoratorMetadata装饰器元数据

类装饰器

类装饰器参数描述

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

下面是一个简单的类装饰器案例,执行后打印就可以发现 value 其实就是 class 本身 这一点和提案二是一样的

ts
// 执行需要 tsc 执行 ts-node v10.9.2 会报错
type constructor<T> = new (...args: any[]) => T;

let o: constructor<A>;

function classDeclaration(v: string) {
  return function <T>(value: constructor<T>, context: ClassDecoratorContext) {
    // 如果是多个不同种类装饰器用这个判断可以进行分类
    if (context.kind === "class") {
      // 还是指向构造函数
      o = value;

      console.log(`class decorator${v}`);
    }
  };
}

@classDeclaration("类装饰器")
class A {}

console.log(A === o); // true

案例 -- 在原型链上添加

ts
// 执行需要 tsc 执行 ts-node v10.9.2 会报错
type constructor<T> = new (...args: any[]) => T;

function classDeclaration(v: string) {
  return function <T>(value: constructor<T>, context: ClassDecoratorContext) {
    // 如果是多个不同种类装饰器用这个判断可以进行分类
    if (context.kind === "class") {
      value.prototype.getName = function () {
        console.log(`class decorator${v}`);
      };
    }
  };
}

@classDeclaration("类装饰器")
class A {}

const a = new A();
(a as any).getName(); // class decorator类装饰器

案例 -- 继承修改修饰类

ts
// 执行需要 tsc 执行 ts-node v10.9.2 会报错
type constructor<T> = new (...args: any[]) => T;

function classDeclaration(v: string) {
  return function <T extends constructor<A>>(
    value: T,
    context: ClassDecoratorContext
  ) {
    // 如果是多个不同种类装饰器用这个判断可以进行分类
    if (context.kind === "class") {
      return class extends value {
        age = 18;
      };
    }
  };
}

@classDeclaration("类装饰器")
class A {}

const a = new A();

console.log(a.constructor); // [Function: class_1] 指向的是子类 此时A 变成了父类
console.log((a as any).age); // 19

覆盖当前类

ts
// 执行需要 tsc 执行 ts-node v10.9.2 会报错
type constructor<T> = new (...args: any[]) => T;

function classDeclaration<T>(v: string) {
  return function (value: constructor<T>, context: ClassDecoratorContext) {
    // 如果是多个不同种类装饰器用这个判断可以进行分类
    if (context.kind === "class") {
			// 这里不能出现 A 中没有的属性否则会 报错
      return class {
        name = "18";
        age = 20;
      };
    }
  };
}

@classDeclaration("类装饰器")
class A {
  name = "w";
}

const a = new A();

console.log(a instanceof A); // true da
console.log((a as any).age); // 19

不准通过new 创建

ts
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'

使用 addInitializer

context的addInitializer()方法,用来定义一个类的初始化函数,在类完全定义结束后执行。类定义完成后,会自动执行类装饰器

ts
// 执行需要 tsc 执行 ts-node v10.9.2 会报错
type constructor<T> = new (...args: any[]) => T;

// 创建一个收集注册定义 class Map

const mapCls = new Map();

function classDeclaration<T>(v: string) {
  return function (value: constructor<T>, context: ClassDecoratorContext) {
    context.addInitializer(function () {
      if (!mapCls.get(value)) mapCls.set(v, value);
    });
  };
}

@classDeclaration("A")
class A {
  name = "w";
}

@classDeclaration("B")
class B {
  name = "ww";
}

// 策略模式分发
function creatClsByKey(k: string) {
  return new (mapCls.get(k))();
}

const a = creatClsByKey("A");

console.log(mapCls); // true
console.log(a.name); // w
  • html 组件class 案例
ts
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>`;
  }
}

方法装饰器

ts
type ClassMethodDecorator = (
  value: Function,
  context: {
    kind: 'method';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { get: () => unknown };
    addInitializer(initializer: () => void): void;
  }
) => Function | void;
  • static:布尔值,表示是否为静态方法。该属性为只读属性。
  • private:布尔值,表示是否为私有方法。该属性为只读属性。
  • access:对象,包含了方法的存取器,但是只有get()方法用来取值,没有set()方法进行赋值。

替代所装饰原始函数

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

function log(value: Function, context: ClassMethodDecoratorContext) {
  console.log(context.static); // 是否是静态方法
  console.log(context.private); // 方法是否为私有

  function replacementMethod(this: any, ...args: any[]) {
    console.log("开始执行");
    const result = value.call(this, ...args);
    return result;
  }

  // 替换原来的函数
  return replacementMethod;
}

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

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

  @log
  getName() {
    return this.name;
  }
}

const robin = new Person("Robin");

console.log(robin.hello()); // 输出: "How are you, Robin?"
console.log(robin.getName()); // 输出: 开始执行 Robin

防抖装饰器

ts
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");

收集

ts
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])

属性装饰器

属性装饰器用来装饰定义在类顶部的属性(field)

ts
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

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

初始化参数值

返回一个函数,该函数会自动执行,用来对所装饰属性进行初始化

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

// class C {
//   @twice
//   field = 3;
// }

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


function initParams(v) {
  return function (value, context: ClassFieldDecoratorContext) {
    const { kind, name } = context;
    if (kind === "field") {
      // console.log(context.access.get());

      return function (initialValue) {
        console.log(
          `initializing ${name as string} with value ${initialValue}`
        );
        return v;
      };
    }
  };
}

class Color {
  @initParams("green")
  name;
}

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

console.log(color.name); // "green"
color.name = "red";
console.log(color.name); // red

用来触发指定对象 get set

ts
const a = { name: "w" };

function initParams(v) {
  return function (value, context: ClassFieldDecoratorContext) {
    const { kind, name } = context;
    if (kind === "field") {
      // console.log(context.access.get());
      // 利用 context.access get set 方法设置触发传入的对象的属性值
      context.access.set(a, v);
      console.log(context.access.get(a), 2); // green 2
    }
  };
}

class Color {
  @initParams("green")
  name;
}

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

console.log(color.name); // "undefined"
color.name = "red";
console.log(color.name); // red
console.log(a.name); // green
console.log(color.name); // red
  • 提取存储器
ts
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'

getter 装饰器,setter 装饰器

ts
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;

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

案例

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

ts
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
// '开销大的计算结果'

参考

TypeScript 5+装饰器变更的影响网道 / WangDoc.com2ality – JavaScript and more

Released under the MIT License.