TypeScript 装饰器详解:提升类和方法功能(十八)

装饰器(Decorators)是 TypeScript 中的一项实验性功能,它允许开发者在不修改原有代码的情况下,为类、方法、属性或参数添加额外的功能。装饰器本质上是一个函数,在运行时被调用,可以修改目标对象的行为。本文将详细介绍如何在 TypeScript 中使用装饰器,并提供一些实用的示例。

装饰器简介

装饰器采用 @ 符号作为语法糖,可以附加在类、方法、访问器、属性或参数上。这种模式在框架开发中非常常见,例如 Angular 和 TypeORM 都大量使用装饰器来实现依赖注入、数据验证等功能。

> 注意:装饰器目前是实验性功能,需要在 tsconfig.json 中显式启用。在生产环境中使用时,请确保项目对实验性特性的支持程度。

配置启用装饰器

在使用装饰器之前,需要在 TypeScript 配置文件 tsconfig.json 中启用相关编译选项。

tsconfig.json 配置

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

参数说明:

  • experimentalDecorators: 启用装饰器语法支持,这是使用装饰器的前提条件。
  • emitDecoratorMetadata: 在编译后的 JavaScript 中生成装饰器的元数据,供依赖注入框架使用。

类装饰器

类装饰器应用于类的构造函数,可以修改类的定义或添加额外的处理逻辑。类装饰器接收一个参数,即目标类的构造函数。

类装饰器基本用法

下面是一个简单的类装饰器示例,展示了如何锁定类的构造函数和原型,防止在运行时添加或删除属性。

// 定义一个类装饰器函数
function sealed(target: Function) {
  // 打印装饰器被应用到的类名
  console.log("装饰器 applied to: " + target.name);

  // 使用 Object.seal 锁定构造函数和原型
  Object.seal(target);
  Object.seal(target.prototype);
}

// 使用 @ 语法将装饰器应用到类上
@sealed
class Person {
  name: string;

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

// 创建实例测试
const person = new Person("RUNOOB");
console.log("创建: " + person.name);

// 尝试添加新属性(会被阻止,因为类被 seal 了)
// person.age = 25; // 静默失败

// 运行结果:
// 装饰器 applied to: Person
// 创建: RUNOOB

类装饰器在类定义时就会执行,通常用于修改类行为、添加元数据或实现 AOP(面向切面编程)。

方法装饰器

方法装饰器应用于类的方法,可以修改方法的属性描述符(Property Descriptor)。方法装饰器接收三个参数:目标对象、属性名称和属性描述符。

方法装饰器基本用法

下面是一个方法装饰器示例,展示了如何设置方法为不可枚举。

// 定义方法装饰器工厂
function enumerable(value: boolean) {
  // 返回装饰器函数,接收三个参数
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // 修改属性的 enumerable 特性
    descriptor.enumerable = value;
  };
}

class Greeter {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  // 应用装饰器,设置该方法为不可枚举
  @enumerable(false)
  greet() {
    return "Hello, " + this.greeting;
  }
}

const g = new Greeter("World");

// 检查 greet 方法是否可枚举
console.log("方法可枚举: " + g.propertyIsEnumerable("greet"));

// 遍历对象的属性
for (const key in g) {
  console.log("属性: " + key);
}

// 运行结果:
// 方法可枚举: false

PropertyDescriptor 包含可枚举(enumerable)、可配置(configurable)、可写(writable)和值(value)等属性,可以根据需要修改。

访问器装饰器

访问器装饰器应用于类的 getter 和 setter 方法。与方法装饰器类似,访问器装饰器也可以修改属性描述符。

访问器装饰器用法

下面是一个访问器装饰器示例,展示了如何锁定 getter。

// 访问器装饰器工厂
function configurable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // 修改属性的 configurable 特性
    descriptor.configurable = value;
  };
}

class Point {
  private _x: number = 0;
  private _y: number = 0;

  // 使用装饰器锁定 getter
  @configurable(false)
  get x() {
    return this._x;
  }

  @configurable(false)
  get y() {
    return this._y;
  }

  set x(value: number) {
    this._x = value;
  }

  set y(value: number) {
    this._y = value;
  }
}

const point = new Point();
point.x = 10;
point.y = 20;

console.log("坐标: (" + point.x + ", " + point.y + ")");

// 说明:访问器装饰器不能同时应用于同一个属性的 getter 和 setter,只能选择其中一个。

属性装饰器

属性装饰器应用于类的属性定义。属性装饰器接收两个参数:目标对象和属性名称。

属性装饰器用法

下面是一个属性装饰器示例,展示了如何为属性添加元数据。

// 属性装饰器工厂
function format(formatString: string) {
  return function (target: any, propertyKey: string) {
    // 在目标对象上存储元数据
    Object.defineProperty(target, propertyKey + "_format", {
      value: formatString,
      writable: false,
      enumerable: false,
      configurable: true
    });
  };
}

class User {
  // 应用属性装饰器,指定日期格式
  @format("YYYY-MM-DD")
  birthDate: string;

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

const user = new User("1990-01-01");
console.log("出生日期: " + user.birthDate);

// 访问存储的元数据
console.log("日期格式: " + (user as any).birthDate_format);

// 运行结果:
// 出生日期: 1990-01-01
// 日期格式: YYYY-MM-DD

参数装饰器

参数装饰器应用于类方法的参数,可以为参数添加元数据或标记。参数装饰器接收三个参数:目标对象、方法名称和参数在函数中的索引。

参数装饰器用法

下面是一个参数装饰器示例,展示了如何记录参数信息。

// 参数装饰器
function logParameter(target: any, propertyKey: string, parameterIndex: number) {
  console.log("参数装饰器: " + propertyKey + " 第 " + (parameterIndex + 1) + " 个参数");
}

class Greeter {
  greeting: string;

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

  // 在参数前使用 @ 语法应用装饰器
  greet(@logParameter name: string) {
    return this.greeting + ", " + name;
  }
}

const greeter = new Greeter("Hello");
greeter.greet("RUNOOB");

// 运行结果:
// 参数装饰器: greet 第 1 个参数

装饰器工厂

装饰器工厂是返回装饰器函数的函数。通过装饰器工厂,可以在应用装饰器时传入自定义参数,实现更灵活的配置。

装饰器工厂实现带颜色的日志

下面是一个装饰器工厂示例,展示了如何实现带颜色的日志。

// 装饰器工厂:接收配置参数,返回装饰器函数
function color(colorCode: string) {
  // colorCode 是 ANSI 转义序列的颜色代码
  // 例如:34 = 蓝色,31 = 红色,32 = 绿色
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // 保存原始方法
    const originalMethod = descriptor.value;

    // 重写方法,添加颜色
    descriptor.value = function (...args: any[]) {
      // 调用原始方法获取返回值
      const result = originalMethod.apply(this, args);

      // 如果在终端环境,给输出添加颜色
      // ANSI 转义序列格式:\x1b[颜色码m 内容 \x1b[0m
      return `\x1b[${colorCode}m${result}\x1b[0m`;
    };
  };
}

class Logger {
  // 使用装饰器工厂,传入蓝色代码 34
  @color("34")
  log(message: string): string {
    return message;
  }

  @color("31")
  error(message: string): string {
    return message;
  }

  @color("32")
  success(message: string): string {
    return message;
  }
}

const logger = new Logger();
console.log(logger.log("这是蓝色日志"));
console.log(logger.error("这是红色错误"));
console.log(logger.success("这是绿色成功"));

// 运行结果:
// 这是蓝色日志(终端显示为蓝色)
// 这是红色错误(终端显示为红色)
// 这是绿色成功(终端显示为绿色)

装饰器工厂是实际开发中最常用的形式,它允许在应用装饰器时传递参数,实现配置化。

装饰器执行顺序

当一个类上有多个装饰器时,执行顺序遵循特定的规则:

  • 装饰器从下往上应用
  • 同一类型的多个装饰器从右到左执行
  • 参数装饰器先于方法装饰器执行

装饰器执行顺序示例

// 多个装饰器叠加使用
function first() {
  console.log("first 装饰器");
  return function (target: any) {
    console.log("first 装饰器函数");
  };
}

function second() {
  console.log("second 装饰器");

  return function (target: any) {
    console.log("second 装饰器函数");
  };
}

@first()
@second()
class MyClass {
  name: string;
}

const obj = new MyClass();

// 运行结果:
// second 装饰器
// first 装饰器
// second 装饰器函数
// first 装饰器函数

装饰器函数先执行定义(console.log),然后按照从下往上的顺序执行装饰器函数。

实际应用场景

装饰器在实际项目中有广泛的应用场景,下面列举几个常见的例子。

日志记录

自动记录方法调用日志。

实例

// 日志装饰器
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log("调用方法: " + propertyKey + ",参数: " + JSON.stringify(args));
    const result = originalMethod.apply(this, args);
    console.log("方法返回: " + JSON.stringify(result));
    return result;
  };
}

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

  @log
  multiply(a: number, b: number): number {
    return a * b;
  }
}

const math = new MathService();
console.log("计算结果: " + math.add(5, 3));

// 运行结果:
// 调用方法: add,参数: [5,3]
// 方法返回: 8
// 计算结果: 8

权限验证

实现方法级别的权限检查。

实例

// 模拟当前用户角色
const currentUser = { role: "admin" };

// 权限装饰器
function requireRole(role: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      if (currentUser.role !== role) {
        console.log("权限不足,无法执行 " + propertyKey);
        return null;
      }
      return originalMethod.apply(this, args);
    };
  };
}

class AdminService {
  @requireRole("admin")
  deleteUser(id: number): string {
    return "删除用户 " + id + " 成功";
  }

  @requireRole("admin")
  viewUser(id: number): string {
    return "查看用户 " + id + " 信息";
  }
}

const adminService = new AdminService();
console.log(adminService.deleteUser(123));

// 运行结果:
// 删除用户 123 成功

总结

装饰器是 TypeScript 中一个强大且灵活的特性,可以帮助开发者在不修改原有代码的情况下,为类、方法、属性或参数添加额外的功能。通过本文的介绍,相信你已经掌握了装饰器的基本用法和一些实际应用场景。希望这些知识能帮助你在项目中更好地利用装饰器,提升代码的可维护性和扩展性。