首页
Preview

TypeScript 5装饰器快速指南

TypeScript 5.0 发布了,官方支持第三阶段的装饰器提案。该提案有四个阶段,这意味着它可以快速稳定下来,而不需要对 API 进行重大更改。在本文中,我们将基于这些稳定的 API 进行探讨。

什么是装饰器

装饰器是 TypeScript 中的一个强大功能,允许开发人员修改或扩展类、方法、访问器和属性的行为。它们提供了一种优雅的方式来添加功能或修改现有构造的行为,而不需要改变它们的原始实现。

装饰器的历史

装饰器在 TypeScript 和 JavaScript 生态系统中有着丰富的历史。装饰器的概念受到了 Python 和其他使用类似构造来修改或扩展类、方法和属性行为的编程语言的启发。JavaScript 的初始装饰器提案于 2014 年推出,自那时以来,已经开发出了几个版本的提案,当前版本处于 ECMAScript 标准化过程的第三阶段。

装饰器的语法

装饰器是以 ‘@’ 符号为前缀的函数,紧接着放置在要修改的构造之前:

@decorator
class MyClass {
  @decorator1
  method() {
    // ...
  }
}

// 同样的效果:你可以将这些装饰器放在同一行
@decorator class MyClass {
  @decorator2 @decorator1 method() {
    // ...
  }
}

装饰器函数及其功能

装饰器是一个函数,它以被装饰的构造为其参数,并可能返回一个修改后的构造或全新的构造。装饰器可以用于:

  • 修改类、方法、访问器或属性的行为
  • 向类或方法添加新功能
  • 为构造提供元数据
  • 强制执行编码标准或最佳实践

类装饰器

类装饰器应用于类构造函数,可用于修改或扩展类的行为。类装饰器的一些常见用例包括:

  • 收集类的实例
  • 冻结类的实例
  • 使类可被调用

示例:收集类的实例

type Constructor<T = {}> = new (...args: any[]) => T;

class InstanceCollector {
  instances = new Set();

  install = <Class extends Constructor>(
    Value: Class,
    context: ClassDecoratorContext<Class>
  ) => {
    const _this = this;
    return class extends Value {
      constructor(...args: any[]) {
        super(...args);
        _this.instances.add(this);
      }
    };
  };
}

const collector = new InstanceCollector();

@collector.install
class Calculator {
  add(a: number, b: number): number {
    return a + b;
  }
}

const calculator1 = new Calculator();
const calculator2 = new Calculator();

console.log('instances: ', collector.instances);

方法装饰器

方法装饰器应用于类方法,可用于修改或扩展方法的行为。方法装饰器的一些常见用例包括:

  • 跟踪方法调用
  • 将方法绑定到实例
  • 应用函数到方法

示例:跟踪方法调用

function log<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<
    This,
    (this: This, ...args: Args) => Return
  >
) {
  const methodName = String(context.name);

  function replacementMethod(this: This, ...args: Args): Return {
    console.log(`LOG: Entering method '${methodName}'.`);
    const result = target.call(this, ...args);
    console.log(`LOG: Exiting method '${methodName}'.`);
    return result;
  }

  return replacementMethod;
}

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

const calculator = new Calculator();
console.log(calculator.add(2, 3));

Getter 和 Setter 装饰器

Getter 和 Setter 装饰器应用于类访问器,允许开发人员修改或扩展它们的行为。Getter 和 Setter 装饰器的一些常见用例包括:

  • 计算值的惰性和缓存
  • 实现只读属性
  • 验证属性赋值

示例:计算值的惰性和缓存

function lazy<This, Return>(
  target: (this: This) => Return,
  context: ClassGetterDecoratorContext<This, Return>
) {
  return function (this: This): Return {
    const value = target.call(this);
    Object.defineProperty(this, context.name, { value, enumerable: true });
    return value;
  };
}

class MyClass {
  private _expensiveValue: number | null = null;

  @lazy
  get expensiveValue(): number {
    this._expensiveValue ??= computeExpensiveValue();
    return this._expensiveValue;
  }
}

function computeExpensiveValue(): number {
  // Expensive computation here…
  console.log('computing...'); // Only call once

  return 42;
}

const obj = new MyClass();

console.log(obj.expensiveValue);
console.log(obj.expensiveValue);
console.log(obj.expensiveValue);

字段装饰器

字段装饰器应用于类字段,可用于修改或扩展字段的行为。字段装饰器的一些常见用例包括:

  • 更改字段的初始化值
  • 实现只读字段
  • 依赖注入
  • 模拟枚举

示例:更改字段的初始化值

function addOne<T>(
  target: undefined,
  context: ClassFieldDecoratorContext<T, number>
) {
  return function (this: T, value: number) {
    console.log('addOne: ', value); // 3
    return value + 1;
  };
}

function addTwo<T>(
  target: undefined,
  context: ClassFieldDecoratorContext<T, number>
) {
  return function (this: T, value: number) {
    console.log('addTwo: ', value); // 1
    return value + 2;
  };
}


class MyClass {
  @addOne
  @addTwo
  x = 1;
}

console.log(new MyClass().x); // 4

这里还有一个额外的知识点,当你堆叠多个装饰器时,它们将以“相反的顺序”运行。在这个例子中,你可以看到 addTwo 中的 1 先被打印出来,然后是 addOne

自动访问器装饰器

自动访问器是一种新的语言特性,简化了 getter 和 setter 对的创建:

class C {
  accessor x = 1;
}


// Same
class C {
  #x = 1;

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

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

这不仅是表达简单访问器对的一种方便方式,还有助于避免装饰器作者尝试在实例上使用访问器替换实例字段时出现的问题,因为当它们被安装在实例上时,ECMAScript 实例字段会遮盖访问器。 它还可以使用装饰器,例如以下只读自动访问器:

function readOnly<This, Return>(
  target: ClassAccessorDecoratorTarget<This, Return>,
  context: ClassAccessorDecoratorContext<This, Return>
) {
  const result: ClassAccessorDecoratorResult<This, Return> = {
    get(this: This) {
      return target.get.call(this);
    },
    set() {
      throw new Error(
        `Cannot assign to read-only property '${String(context.name)}'.`
      );
    },
  };

  return result;
}

class MyClass {
  @readOnly accessor myValue = 123;
}

const obj = new MyClass();

console.log(obj.myValue);
obj.myValue = 456; // Error: Cannot assign to read-only property 'myValue'.
console.log(obj.myValue);

结论

装饰器是 TypeScript 中的一个强大功能,提供了一种优雅的方式来修改或扩展类、方法、访问器和属性的行为。随着装饰器提案的不断发展和成熟,你可以逐步选择将其应用于你的项目。

参考文献

[1] https://github.com/microsoft/TypeScript/pull/50820

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
明浩
北漂一族

评论(0)

添加评论