typescript中的装饰器@及使用场景

216 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第14天,点击查看活动详情

TypeScript 中的装饰器(Decorator)是一种特殊语法,用于在类、方法、属性或参数上添加元数据或修改它们的行为。

装饰器本质上是一个函数,它可以在运行时被调用,并接收特定的参数来对目标进行修饰。

TypeScript 装饰器的实现基于 ECMAScript 装饰器提案,但目前 TypeScript 的实现与标准提案可能有所不同。

接下来,我会从基础到高级逐步介绍 TypeScript 中的装饰器,包括如何装饰类、方法、属性以及函数参数。

1. 装饰器基础

1.1 什么是装饰器?

装饰器是一个函数,它可以被附加到类、方法、属性或参数上,用来修改或扩展它们的行为。装饰器的语法是以 @ 开头的函数调用。

1.2 启用装饰器

在 TypeScript 中使用装饰器,需要在 tsconfig.json 中启用 experimentalDecorators 选项:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
  • experimentalDecorators:启用装饰器语法。
  • emitDecoratorMetadata:生成装饰器的元数据(可选)。

2. 类装饰器

2.1 类装饰器的定义

类装饰器是一个函数,它接收类的构造函数作为参数,可以用来修改或扩展类的行为。

当装饰器装饰类的时候,有如下规则:

  • 装饰器本身是一个函数
  • 装饰器接受的参数必须是一个构造函数,也就是类本身
  • 装饰器通过@符号来调用
  • 装饰器的执行时机是在类创建好之后立即执行,而不是在new的时候执行

2.2 类装饰器的使用

function ClassDecorator(constructor: Function) {
  console.log("Class Decorator called");
  // 可以在这里修改类的原型或静态属性
  constructor.prototype.newProperty = "This is a new property";
}

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

const instance = new MyClass();
console.log((instance as any).newProperty); // 输出: This is a new property

输出:

Class Decorator called
MyClass instance created
This is a new property

有的时候,我想要执行装饰器,但是有的时候我不想,那么就可以使用工厂模式来做。如下代码,我们就执行了一个函数,并且传入一个参数,根据参数来控制返回的装饰器函数。

function testDecorator(flag: boolean) {
  if (flag) {
    return function (constructor: any) {
      constructor.prototype.getName = () => {
        console.log('dell')
      }
    }
  } else {
    // 空函数,什么都不做
    return function (constructor: any) {}
  }
}

@testDecorator(true)
class Test {}

const test = new Test()
;(test as any).getName()

2.3 类装饰器的应用场景

  • 添加元数据。
  • 修改类的原型。
  • 实现依赖注入。

上面演示了如何修改类的原型,这里简要讲下什么是依赖注入及如何实现。

依赖注入(Dependency Injection,简称 DI)  是一种设计模式,用于实现 控制反转(Inversion of Control,IoC)

它的核心思想是将对象的依赖关系由外部容器或框架来管理,而不是在对象内部直接创建依赖

通过依赖注入,代码的耦合度降低,模块化和可测试性增强。依赖注入广泛应用于现代框架中,如 Angular、NestJS、Spring 等。

在编程中,一个类或对象可能需要依赖其他类或对象来完成某些功能。例如:

class Engine {
  start() {
    console.log("Engine started");
  }
}

class Car {
  private engine: Engine;

  constructor() {
    this.engine = new Engine(); // Car 依赖 Engine
  }

  start() {
    this.engine.start();
    console.log("Car started");
  }
}

const car = new Car();
car.start();

在上面的代码中,Car 类直接创建了 Engine 的实例,这意味着 Car 和 Engine 是紧耦合的。

依赖注入的思想是:将依赖的创建和管理交给外部容器,而不是在类内部直接创建依赖。例如:

class Engine {
  start() {
    console.log("Engine started");
  }
}

class Car {
  private engine: Engine;

  constructor(engine: Engine) { // 通过构造函数注入依赖
    this.engine = engine;
  }

  start() {
    this.engine.start();
    console.log("Car started");
  }
}

const engine = new Engine();
const car = new Car(engine); // 将 Engine 实例注入 Car
car.start();

在这个例子中,Car 不再直接创建 Engine,而是通过构造函数接收 Engine 的实例。这就是依赖注入的核心思想。

依赖注入的三种方式:

1. 构造函数注入

通过构造函数传递依赖(最常用的方式)。

class Engine {
  start() {
    console.log("Engine started");
  }
}

class Car {
  constructor(private engine: Engine) {} // 构造函数注入

  start() {
    this.engine.start();
    console.log("Car started");
  }
}

const engine = new Engine();
const car = new Car(engine);
car.start();

2. 属性注入

通过属性赋值传递依赖。

class Engine {
  start() {
    console.log("Engine started");
  }
}

class Car {
  engine: Engine; // 属性注入
  
  start() {
    this.engine.start();
    console.log("Car started");
  }
}

const engine = new Engine();
const car = new Car();
car.engine = engine; // 设置依赖
car.start();

3. 方法注入

通过方法参数传递依赖。

class Engine {
  start() {
    console.log("Engine started");
  }
}

class Car {
  start(engine: Engine) { // 方法注入
    engine.start();
    console.log("Car started");
  }
}

const engine = new Engine();
const car = new Car();
car.start(engine); // 传递依赖

下面看一下依赖注入在 NestJS 中的应用,NestJS 是一个基于 TypeScript 的后端框架,也广泛使用依赖注入。

import { Injectable } from '@nestjs/common';

@Injectable()
export class Engine {
  start() {
    console.log("Engine started");
  }
}

@Injectable()
export class Car {
  constructor(private readonly engine: Engine) {} // 依赖注入:构造函数注入
  
  @Inject(JwtService) //  依赖注入:属性注入
  private JwtService: JwtService
  
  start() {
    this.engine.start();
    console.log("Car started");
  }
}

以下是一个简单的依赖注入容器的实现:

class Container {
  private instances = new Map<string, any>();

  register(key: string, instance: any) {
    this.instances.set(key, instance);
  }

  resolve<T>(key: string): T {
    const instance = this.instances.get(key);
    if (!instance) {
      throw new Error(`No instance found for key: ${key}`);
    }
    return instance;
  }
}

class Engine {
  start() {
    console.log("Engine started");
  }
}

class Car {
  constructor(private engine: Engine) {}

  start() {
    this.engine.start();
    console.log("Car started");
  }
}

使用容器管理依赖
const container = new Container();
container.register("engine", new Engine());
container.register("car", new Car(container.resolve("engine")));

实例的创建都是容器创建的
const car = container.resolve<Car>("car");
car.start();

3. 方法装饰器

3.1 方法装饰器的定义

首先要强调装饰器对普通的方法无效,只能对类里面的方法有效

方法装饰器是一个函数,它接收以下参数:

  • 第一个参数:如果是通用方法,target 对应的是类的 prototype,如果是静态方法static,对应的是类的构造函数
  • 第二个参数:就是装饰的方法名
  • 第三个参数:类似于Object.defineProperty的descriptor,可以设置属性是否可写,可读等操作

3.2 方法装饰器的使用

function MethodDecorator(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  console.log("Method Decorator called");
  console.log("Target:", target);
  console.log("Property Key:", propertyKey);
  console.log("Descriptor:", descriptor);

  // 修改方法的行为
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Method ${propertyKey} is called with args:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`Method ${propertyKey} returned:`, result);
    return result;
  };
}

class MyClass {
  @MethodDecorator
  myMethod(arg1: string, arg2: number) {
    console.log("Executing myMethod");
    return `Received: ${arg1}, ${arg2}`;
  }
}

const instance = new MyClass();
instance.myMethod("Hello", 42);

输出:

Method Decorator called
Target: MyClass { myMethod: [Function] }
Property Key: myMethod
Descriptor: {
  value: [Function],
  writable: true,
  enumerable: true,
  configurable: true
}
Method myMethod is called with args: [ 'Hello', 42 ]
Executing myMethod
Method myMethod returned: Received: Hello, 42

3.3 方法装饰器的应用场景

  • 日志记录。
  • 性能监控。
  • 方法拦截(如权限检查)。

首先看一个日志记录的场景:

class Test {
  userInfo: any = undefined
  getName() {
    return this.userInfo.name
  }
  getAge() {
    return this.userInfo.age
  }
}
const test = new Test()
test.getName()
test.getAge()

当访问getName或者getAge的时候,就会报错,因为userInfo是undefined,那我们修改下:

class Test {
  userInfo: any = undefined
  getName() {
    try {
      return this.userInfo.name
    } catch (err) {
      console.log(err)
    }
  }
  getAge() {
    try {
      return this.userInfo.age
    } catch (err) {
      console.log(err)
    }
  }
}

如果有很多这种方法,那么我们就要写很多try catch,这样代码就变得非常的长,这个时候就可以使用装饰器了。

function catchError(target: any, key: string, descriptor: PropertyDescriptor) {
  const fn = descriptor.value
  descriptor.value = function () {
    try {
      fn()
    } catch (e) {
      console.log('userInfo 存在问题')
    }
  }
}

class Test {
  userInfo: any = undefined
  @catchError
  getName() {
    return this.userInfo.name
  }
  @catchError
  getAge() {
    return this.userInfo.age
  }
}

通过装饰器,就可以节省大量的代码,但是还有个问题,报错信息要从外部传进去,那怎么做呢?

function catchError(msg: string) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const fn = descriptor.value
    descriptor.value = function () {
      try {
        fn()
      } catch (e) {
        console.log(msg)
      }
    }
  }
}

class Test {
  userInfo: any = undefined
  @catchError('userInfo.name 不存在')
  getName() {
    return this.userInfo.name
  }
  @catchError('userInfo.age 不存在')
  getAge() {
    return this.userInfo.age
  }
  @catchError('userInfo.gender 不存在')
  getGender() {
    return this.userInfo.gender
  }
}

再来看看方法装饰器是如何用于实现方法拦截(Method Interception)。

简单来说,是在方法执行前后插入额外的逻辑,比如权限检查、缓存、日志记录等。通过方法装饰器,我们可以轻松地拦截方法的调用,并在必要时修改方法的行为。

function Intercept(interceptor: { before?: Function; after?: Function }) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // 保存原始方法
    const originalMethod = descriptor.value;

    // 重写方法
    descriptor.value = function (...args: any[]) {
      // 执行前置拦截逻辑
      if (interceptor.before) {
        interceptor.before(args);
      }

      // 调用原始方法
      const result = originalMethod.apply(this, args);

      // 执行后置拦截逻辑
      if (interceptor.after) {
        interceptor.after(result);
      }

      // 返回原始方法的结果
      return result;
    };

    return descriptor;
  };
}

class MyClass {
  @Intercept({
    before: (args: any[]) => {
      console.log(`[BEFORE] Method is about to be called with args: ${JSON.stringify(args)}`);
    },
    after: (result: any) => {
      console.log(`[AFTER] Method returned: ${JSON.stringify(result)}`);
    },
  })
  myMethod(arg1: string, arg2: number) {
    console.log("Executing myMethod");
    return `Received: ${arg1}, ${arg2}`;
  }
}

const instance = new MyClass();
instance.myMethod("Hello", 42);

运行上述代码后,控制台会输出以下日志:

[BEFORE] Method is about to be called with args: ["Hello",42]
Executing myMethod
[AFTER] Method returned: "Received: Hello, 42"

4. 属性装饰器

4.1 属性装饰器的定义

属性装饰器是一个函数,它接收以下参数:

  1. 类的原型(如果是静态属性,则是类的构造函数)。
  2. 属性名称。

4.2 属性装饰器的使用

function PropertyDecorator(target: any, propertyKey: string) {
  console.log("Property Decorator called");
  console.log("Target:", target);
  console.log("Property Key:", propertyKey);

  // 可以在这里修改属性的行为
  let value: any;
  const getter = () => {
    console.log(`Getting value: ${value}`);
    return value;
  };
  const setter = (newValue: any) => {
    console.log(`Setting value: ${newValue}`);
    value = newValue;
  };

  // 替换属性的 getter 和 setter
  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true,
  });
}

class MyClass {
  @PropertyDecorator
  myProperty: string;
}

const instance = new MyClass();
instance.myProperty = "Hello";
console.log(instance.myProperty);

输出:

Property Decorator called
Target: MyClass {}
Property Key: myProperty
Setting value: Hello
Getting value: Hello
Hello

4.3 属性装饰器的应用场景

  • 属性验证。
  • 属性监听(如 Vue 的响应式系统)。

通过在属性赋值时触发验证逻辑,我们可以确保属性的值符合特定的规则或约束。

以下是一个简单的实现,展示如何使用属性装饰器实现属性验证:

function Validate(validationFn: (value: any) => boolean) {
  return function (target: any, propertyKey: string) {
    let value: any;

    // 定义属性的 getter 和 setter
    const getter = () => value;
    const setter = (newValue: any) => {
      // 执行验证逻辑
      if (!validationFn(newValue)) {
        throw new Error(`Invalid value for property ${propertyKey}: ${newValue}`);
      }
      value = newValue;
    };

    // 替换属性的 getter 和 setter
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    });
  };
}

class MyClass {
  @Validate((value: number) => value > 0) // 验证值必须大于 0
  myProperty: number;
}

const instance = new MyClass();

// 合法赋值
instance.myProperty = 42;
console.log(instance.myProperty); // 输出: 42

// 非法赋值
try {
  instance.myProperty = -1; // 抛出错误
} catch (error) {
  console.error(error.message); // 输出: Invalid value for property myProperty: -1
}

5. 参数装饰器

5.1 参数装饰器的定义

参数装饰器是一个函数,它接收以下参数:

  1. 类的原型(如果是静态方法,则是类的构造函数)。
  2. 方法名称。
  3. 参数在函数参数列表中的索引。

5.2 参数装饰器的使用

function ParameterDecorator(target: any, methodName: string, parameterIndex: number) {
  console.log("Parameter Decorator called");
  console.log("Target:", target);
  console.log("Method Name:", methodName);
  console.log("Parameter Index:", parameterIndex);
}

class MyClass {
  myMethod(@ParameterDecorator arg1: string, arg2: number) {
    console.log(`Executing myMethod with args: ${arg1}, ${arg2}`);
  }
}

const instance = new MyClass();
instance.myMethod("Hello", 42);

输出:

Parameter Decorator called
Target: MyClass { myMethod: [Function] }
Method Name: myMethod
Parameter Index: 0
Executing myMethod with args: Hello, 42

5.3 参数装饰器的应用场景

  • 参数验证。
  • 依赖注入。

下面来看一个例子,参数装饰器如何实现依赖注入的。

import "reflect-metadata";

// 定义参数装饰器
function Inject(token: string) {
  return function (target: any, methodName: string, parameterIndex: number) {
    Reflect.defineMetadata("inject", token, target, `${methodName}_${parameterIndex}`);
  };
}

// 定义服务类
class MyService {
  doSomething() {
    console.log("MyService is doing something");
  }
}

// 定义需要注入的类
class MyClass {
  constructor(@Inject("MyService") private myService: MyService) {}

  doSomething() {
    this.myService.doSomething();
  }
}

定义依赖注入容器
class Container {
  private instances = new Map<string, any>();

  register(token: string, instance: any) {
    this.instances.set(token, instance);
  }

  resolve<T>(token: string): T {
    const instance = this.instances.get(token);
    if (!instance) {
      throw new Error(`No instance found for token: ${token}`);
    }
    return instance;
  }

  inject<T>(target: any): T {
    const instance = new target();
    
    在 TypeScript 里,`design:paramtypes` 是一种由 TypeScript 编译器自动生成的元数据,
    借助反射机制,它能让我们在运行时获取构造函数参数的类型。
    这里会返回构造函数参数类型数组,即[MyService]
    const paramTypes = Reflect.getMetadata("design:paramtypes", target) || [];
    for (let i = 0; i < paramTypes.length; i++) {
      const token = Reflect.getMetadata("inject", target.prototype, `constructor_${i}`);
      if (token) {
        const dependency = this.resolve(token);
        把MyService实例赋值给MyClass的属性myService上,这样就完成了依赖注入
        instance[i] = dependency;
      }
    }

    return instance;
  }
}

// 使用依赖注入
const container = new Container();
container.register("MyService", new MyService());

// 通过container.inject实例化
const myClassInstance = container.inject(MyClass);
myClassInstance.doSomething(); // 输出: MyService is doing something

可以看到参数装饰器@Inject("MyService")通过元数据添加了token,在实例化MyClass时,通过Reflect.getMetadata取出元数据,并获取元数据对应实例MyService,在MyClass实例化过程中赋值给MyClass中的myService属性。

6. 装饰器的执行顺序

在 TypeScript 中,装饰器的执行顺序是一个非常重要的概念,尤其是当多个装饰器同时应用于类、方法、属性或参数时。

    1. 同一目标的多个装饰器

如果有多个装饰器应用于同一个目标(如类、方法、属性等),它们的执行顺序是从下到上(从最接近目标的装饰器开始)。

@DecoratorA
@DecoratorB
class MyClass {}

2. 不同类型的装饰器

如果类、方法、属性、参数等都有装饰器,它们的执行顺序是:

  1. 参数装饰器(Parameter Decorators)。
  2. 方法装饰器(Method Decorators)。
  3. 属性装饰器(Property Decorators)。
  4. 类装饰器(Class Decorators)。
function ClassDecorator(target: Function) {
    console.log("Class Decorator applied");
}

function MethodDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
    console.log("Method Decorator applied");
}

function PropertyDecorator(target: any, key: string) {
    console.log("Property Decorator applied");
}

function ParameterDecorator(target: any, key: string, parameterIndex: number) {
    console.log("Parameter Decorator applied");
}

@ClassDecorator
class MyClass {
    @PropertyDecorator
    myProperty: string;

    @MethodDecorator
    myMethod(@ParameterDecorator param: string) {}
}

// 输出:
// Parameter Decorator applied
// Method Decorator applied
// Property Decorator applied
// Class Decorator applied