装饰器

装饰器(Decorator)是一种特殊的声明,它允许我们在类、方法、属性等定义上附加元数据或修改其行为。装饰器在很多框架中都有广泛应用,比如Angular、NestJS等。TypeScript很早就支持了装饰器语法,但早期版本的装饰器是基于Stage 2阶段的提案实现的,属于实验性特性。TypeScript 5.0正式引入了符合ECMAScript Stage 3提案的新版装饰器,这个版本的装饰器无需额外配置即可使用,语法和行为也与旧版有所不同。本篇笔记将介绍TypeScript 5.0新版装饰器的用法。

新旧装饰器的区别

在TypeScript 5.0之前,使用装饰器需要在tsconfig.json中如下配置。

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

新版装饰器则无需任何配置即可直接使用,两个版本的装饰器在语法和运行时行为上存在差异,它们并不兼容!如果项目中使用了依赖旧版装饰器的框架(如Angular、NestJS等),仍需保持旧版配置,不可混用。两个版本的装饰器主要区别如下。

特性 旧版装饰器 新版装饰器
提案阶段 Stage 2 Stage 3
配置要求 需要experimentalDecorators 无需配置
参数签名 因装饰目标不同而各异 统一的(target, context)签名
元数据支持 需要reflect-metadata 原生支持metadata
执行顺序 自下而上 自下而上(与旧版一致)

装饰器的基本语法

装饰器本质上是一个函数,它接收被装饰的目标和上下文信息作为参数,下面是一个最简单的类装饰器示例,我们装饰了一个类的构造函数。

function LogClass(target: Function, context: ClassDecoratorContext) {
  console.log(`Class ${context.name} is decorated`);
}

@LogClass
class MyClass {
  constructor() {
    console.log("MyClass instance created");
  }
}

const instance = new MyClass();

运行上面的代码,输出结果如下。

Class MyClass is decorated
MyClass instance created

注意这里装饰器在类定义时就会执行,而不是在实例化时执行。

类装饰器

类装饰器用于装饰类声明,它可以用来修改类的行为或添加额外的功能。

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

function Timestamped<T extends Constructor>(
  target: T,
  context: ClassDecoratorContext
) {
  return class extends target {
    createdAt = new Date();
  };
}

@Timestamped
class Document {
  title: string;

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

const doc = new Document("My Document");
console.log(doc.title);
console.log((doc as any).createdAt);

上面例子中,Timestamped装饰器为类添加了一个createdAt属性,记录实例创建的时间。

方法装饰器

方法装饰器用于装饰类的方法,常用于日志记录、性能监控、权限检查等场景。

function Log(
  target: Function,
  context: ClassMethodDecoratorContext
) {
  const methodName = String(context.name);

  return function (this: any, ...args: any[]) {
    console.log(`Calling ${methodName} with args:`, args);
    const result = target.apply(this, args);
    console.log(`Method ${methodName} returned:`, result);
    return result;
  };
}

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

const calc = new Calculator();
calc.add(2, 3);

运行输出结果如下。

Calling add with args: [2, 3]
Method add returned: 5

属性装饰器

属性装饰器用于装饰类的属性,可以用来实现属性验证、默认值设置等功能。

function DefaultValue(value: any) {
  return function (
    target: undefined,
    context: ClassFieldDecoratorContext
  ) {
    return function (initialValue: any) {
      return initialValue ?? value;
    };
  };
}

class Settings {
  @DefaultValue("guest")
  username: string;

  @DefaultValue(8080)
  port: number;
}

const settings = new Settings();
console.log(settings.username); // 输出: guest
console.log(settings.port); // 输出: 8080

注意新版属性装饰器的target参数是undefined,这与旧版装饰器不同。

访问器装饰器

访问器装饰器用于装饰类的gettersetter

function Validate(
  target: Function,
  context: ClassGetterDecoratorContext | ClassSetterDecoratorContext
) {
  if (context.kind === "setter") {
    return function (this: any, value: any) {
      if (typeof value !== "number" || value < 0) {
        throw new Error("Value must be a non-negative number");
      }
      target.call(this, value);
    };
  }
  return target;
}

class Product {
  private _price: number = 0;

  get price(): number {
    return this._price;
  }

  @Validate
  set price(value: number) {
    this._price = value;
  }
}

const product = new Product();
product.price = 100; // 正常
console.log(product.price); // 输出: 100

try {
  product.price = -10; // 抛出错误
} catch (e) {
  console.error(e.message); // 输出: Value must be a non-negative number
}

装饰器工厂

装饰器工厂是一个返回装饰器函数的高阶函数,它允许我们在使用装饰器时传递参数。

function Retry(maxAttempts: number, delay: number) {
  return function (
    target: Function,
    context: ClassMethodDecoratorContext
  ) {
    return async function (this: any, ...args: any[]) {
      let lastError: Error;

      for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
          return await target.apply(this, args);
        } catch (error) {
          lastError = error as Error;
          console.log(`Attempt ${attempt} failed, retrying...`);
          if (attempt < maxAttempts) {
            await new Promise((resolve) => setTimeout(resolve, delay));
          }
        }
      }

      throw lastError!;
    };
  };
}

class ApiClient {
  private failCount = 0;

  @Retry(3, 1000)
  async fetchData(): Promise<string> {
    this.failCount++;
    if (this.failCount < 3) {
      throw new Error("Network error");
    }
    return "Data fetched successfully";
  }
}

const client = new ApiClient();
client.fetchData().then(console.log).catch(console.error);

装饰器组合

多个装饰器可以同时应用于同一个目标,它们的执行顺序是自下而上(从最接近目标的装饰器开始)。

function First(
  target: Function,
  context: ClassMethodDecoratorContext
) {
  console.log("First decorator evaluated");
  return function (this: any, ...args: any[]) {
    console.log("First decorator executed");
    return target.apply(this, args);
  };
}

function Second(
  target: Function,
  context: ClassMethodDecoratorContext
) {
  console.log("Second decorator evaluated");
  return function (this: any, ...args: any[]) {
    console.log("Second decorator executed");
    return target.apply(this, args);
  };
}

class Example {
  @First
  @Second
  method() {
    console.log("Method executed");
  }
}

const example = new Example();
example.method();

输出顺序如下。

Second decorator evaluated
First decorator evaluated
First decorator executed
Second decorator executed
Method executed

装饰器的求值顺序是自下而上的,但包装后的函数执行顺序是自上而下的(类似洋葱模型)。

上下文对象

新版装饰器的第二个参数是上下文对象(context),它提供了关于被装饰目标的元信息。

interface DecoratorContext {
  kind: "class" | "method" | "getter" | "setter" | "field" | "accessor";
  name: string | symbol;
  static: boolean;
  private: boolean;
  access?: {
    get?(): unknown;
    set?(value: unknown): void;
  };
  addInitializer?(initializer: () => void): void;
  metadata: DecoratorMetadata;
}

其中addInitializer方法允许我们注册一个初始化函数,它会在类实例化时执行。

function AutoBind(
  target: Function,
  context: ClassMethodDecoratorContext
) {
  const methodName = context.name;

  context.addInitializer(function (this: any) {
    this[methodName] = this[methodName].bind(this);
  });

  return target;
}

class Button {
  label = "Click me";

  @AutoBind
  handleClick() {
    console.log(`Button: ${this.label}`);
  }
}

const button = new Button();
const handler = button.handleClick;
handler(); // 输出: Button: Click me(this已正确绑定)

装饰器的实际应用场景

装饰器在实际开发中有很多应用场景:

  • 日志记录:自动记录方法的调用和返回值。
  • 性能监控:测量方法的执行时间。
  • 缓存:缓存方法的返回结果。
  • 权限验证:检查用户权限后再执行方法。
  • 依赖注入:自动注入依赖项。
  • 数据验证:验证方法参数或属性值。
  • 单例模式:确保类只有一个实例。

下面是一个缓存装饰器的示例。

function Memoize(
  target: Function,
  context: ClassMethodDecoratorContext
) {
  const cache = new Map<string, any>();

  return function (this: any, ...args: any[]) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      console.log("Cache hit");
      return cache.get(key);
    }
    console.log("Cache miss");
    const result = target.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

class MathService {
  @Memoize
  fibonacci(n: number): number {
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

const math = new MathService();
console.log(math.fibonacci(10)); // 第一次计算
console.log(math.fibonacci(10)); // 从缓存获取
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。