一坤时学习 TS 中的装饰器,让你写 NestJS 不再手软 😏😏😏

面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:yunmz777

在 TypeScript 中,装饰器(Decorator)是一种特殊的声明,能够附加到类、方法、访问器、属性或参数上,用来修改它们的行为。装饰器本质上是一个函数,通过函数的调用和传递特定的参数,你可以动态地为目标添加元数据、修改行为或执行其他操作。

如何使用

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

diff 复制代码
{
  "compilerOptions": {
    "target": "ES5",
    "module": "CommonJS",
    "lib": ["ES6", "DOM"],
+   "experimentalDecorators": true,
+   "emitDecoratorMetadata": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

装饰器类型

Typescript 中主要有以下几种装饰器:

  1. 类装饰器

  2. 方法装饰器

  3. 访问装饰器

  4. 属性装饰器

  5. 参数装饰器

类装饰器

类装饰器使用 @expression 语法,其中 expression 必须计算为一个函数,该函数在运行时被调用,类的构造函数作为其唯一参数。

ts 复制代码
function classDecorator(constructor: Function) {
  console.log("类装饰器被调用");
  // 可以修改构造函数或原型
}

@classDecorator
class Example {
  constructor() {
    console.log("Example 类被实例化");
  }
}

// 输出: "类装饰器被调用"
const instance = new Example();
// 输出: "Example 类被实例化"

输出结果如下图所示:

类装饰器接收类的构造函数作为其唯一参数,如下代码所示:

ts 复制代码
function classDecorator(constructor: any) {
  console.log(`装饰的类名: ${constructor.name}`);
  // 可以访问和修改构造函数的属性和方法
}

@classDecorator
class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}
// 输出: "装饰的类名: User"

输出结果如下所示:

借助类装饰器我们可以通过修改类的原型来添加新的属性和方法,如下代码所示:

ts 复制代码
function addProperties(constructor: Function) {
  // 向原型添加方法
  constructor.prototype.newMethod = function () {
    return "这是通过装饰器添加的新方法";
  };

  // 向原型添加属性
  constructor.prototype.newProperty = "这是通过装饰器添加的新属性";

  // 向类添加静态属性
  constructor.staticProperty = "这是静态属性";
}

@addProperties
class MyClass {
  originalMethod() {
    return "原始方法";
  }
}

const obj = new MyClass();
console.log((obj as any).newProperty); // "这是通过装饰器添加的新属性"
console.log((obj as any).newMethod()); // "这是通过装饰器添加的新方法"
console.log((MyClass as any).staticProperty); // "这是静态属性"

如下代码输出结果所示:

类装饰器可以通过返回一个新的构造函数来完全替换原始类:

ts 复制代码
function replaceClass<T extends { new (...args: any[]): {} }>(constructor: T) {
  // 返回一个新的类,继承自原始类
  return class extends constructor {
    newProperty = "新属性";

    // 重写构造函数
    constructor(...args: any[]) {
      super(...args);
      console.log("增强的构造函数被调用");
    }

    // 添加新方法
    newMethod() {
      return "新方法";
    }
  };
}

@replaceClass
class OriginalClass {
  originalProperty = "原始属性";

  constructor() {
    console.log("原始构造函数被调用");
  }

  originalMethod() {
    return "原始方法";
  }
}

const instance = new OriginalClass();
// 输出:
// "原始构造函数被调用"
// "增强的构造函数被调用"

console.log(instance.originalProperty); // "原始属性"
console.log((instance as any).newProperty); // "新属性"
console.log(instance.originalMethod()); // "原始方法"
console.log((instance as any).newMethod()); // "新方法"

如下输出结果所示:

可以使用类装饰器来监控类的实例化和方法调用:

ts 复制代码
function monitor<T extends { new (...args: any[]): {} }>(constructor: T) {
  // 返回一个代理类
  return class extends constructor {
    constructor(...args: any[]) {
      console.log(`创建 ${constructor.name} 的实例,参数:`, args);
      super(...args);
      console.log(`${constructor.name} 实例创建完成`);
    }
  };
}

@monitor
class Person {
  constructor(public name: string, public age: number) {
    console.log("Person 构造函数执行");
  }
}

const person = new Person("张三", 30);
// 输出:
// "创建 Person 的实例,参数: ["张三", 30]"
// "Person 构造函数执行"
// "Person 实例创建完成"

如下输出结果所示:

类装饰器应用场景

借助类装饰器,我们可以实现单例模式:

ts 复制代码
function singleton<T extends { new (...args: any[]): {} }>(constructor: T) {
  // 保存原始构造函数
  const originalConstructor = constructor;

  // 创建一个新的函数来管理实例
  const newConstructor: any = function (...args: any[]) {
    // 检查是否已经有实例
    if (!newConstructor.instance) {
      newConstructor.instance = new originalConstructor(...args);
    }
    return newConstructor.instance;
  };

  // 复制原型链
  newConstructor.prototype = originalConstructor.prototype;

  return newConstructor as T;
}

@singleton
class Database {
  private connectionString: string;

  constructor(connectionString: string) {
    this.connectionString = connectionString;
    console.log(`连接到数据库: ${connectionString}`);
  }

  query(sql: string) {
    console.log(`执行查询: ${sql}`);
    return `查询结果: ${sql}`;
  }
}

// 创建第一个实例
const db1 = new Database("mongodb://localhost:27017");
// 输出: "连接到数据库: mongodb://localhost:27017"

// 创建第二个实例 - 不会再次连接
const db2 = new Database("mongodb://localhost:27018");
// 没有输出,因为使用的是第一个实例

console.log(db1 === db2); // true - 两个变量引用同一个实例

在上面的代码中,通过 @singleton 装饰器实现了单例模式,使得无论创建多少次 Database 实例,都只会返回第一个实例。@singleton 装饰器通过拦截构造函数调用,检查实例是否已存在,如果不存在则创建新实例,否则返回已存在的实例。这样设计的好处是节省资源(如数据库连接)并确保实例唯一性,适合在应用中共享全局状态或服务。

最终输出结果如下图所示:

我们借助装饰器,还可以实现类的混入(Mixins),混入的核心思想是将一个或多个源类的功能"混合"到目标类中。与继承不同,混入不建立 "is-a" 关系,而是提供 "has-a" 功能。

ts 复制代码
// 定义构造函数类型
type Constructor<T = {}> = new (...args: any[]) => T;

// 创建混入工厂
function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    timestamp = Date.now();

    getTimestamp() {
      return this.timestamp;
    }
  };
}

function Activatable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    isActive = false;

    activate() {
      this.isActive = true;
      return this;
    }

    deactivate() {
      this.isActive = false;
      return this;
    }
  };
}

// 创建装饰器工厂
function ApplyMixins(...mixins: Array<(base: Constructor) => Constructor>) {
  return function <T extends Constructor>(Base: T) {
    return mixins.reduce(
      (AccumulatedBase, Mixin) => Mixin(AccumulatedBase),
      Base
    );
  };
}

// 使用装饰器应用混入
@ApplyMixins(Timestamped, Activatable)
class User {
  constructor(public name: string) {}

  greet() {
    return `Hello, I'm ${this.name}`;
  }
}

// 使用混入后的类
const user = new User("李四");
console.log(user.greet()); // "Hello, I'm 李四"
console.log(user.getTimestamp()); // 时间戳
console.log(user.isActive); // false
(user as any).activate();
console.log(user.isActive); // true

这段代码实现了 TypeScript 中的混入(Mixins)模式,通过定义功能接口和混入函数,创建了一个装饰器工厂来将多个功能(时间戳和激活状态)组合到 User 类中,实现了代码复用而不依赖传统继承,同时保持了类型安全。

它的最终输出结果如下图所示:

类装饰器执行时机

类装饰器在类定义时执行,而不是在类实例化时执行。这意味着装饰器代码会在程序启动时就运行,而不是在创建类实例时运行。

ts 复制代码
function logClass(constructor: Function) {
  console.log(`类 ${constructor.name} 被定义`);
  return constructor;
}

@logClass
class Example {}

console.log("程序开始执行");
// 输出:
// "类 Example 被定义"
// "程序开始执行"

如下输出结果所示:

小结

类装饰器是 TypeScript 中强大的元编程工具,它允许我们在不修改原始代码的情况下扩展或修改类的行为。通过类装饰器,我们可以实现各种设计模式,如单例、依赖注入、混入等,同时保持代码的清晰和可维护性。

类装饰器的主要优势在于它们提供了一种声明式的方式来添加功能,使代码更具表达力和可读性。在现代 TypeScript 框架中,类装饰器已经成为标准工具,用于实现各种高级功能。

方法装饰器

方法装饰器是 TypeScript 装饰器中最常用的类型之一,它应用于类的方法定义,可以用来观察、修改或替换方法的定义。方法装饰器在运行时被调用,可以用来拦截方法的调用、添加额外的行为,或者完全改变方法的实现。

方法装饰器声明在方法声明之前,使用 @expression 形式,其中 expression 必须计算为一个函数,该函数在运行时被调用。

typescript 复制代码
class Example {
  @methodDecorator
  method() {
    // 方法实现
  }
}

方法装饰器函数接收三个参数:

  1. target: 对于实例方法,是类的原型对象;对于静态方法,是类的构造函数

  2. propertyKey: 方法的名称

  3. descriptor: 方法的属性描述符(PropertyDescriptor)

typescript 复制代码
function methodDecorator(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  // 装饰器实现
}

属性描述符(PropertyDescriptor)是 JavaScript 中用于描述对象属性特性的对象,包含以下属性:

  • value: 属性的值(在方法装饰器中,这是方法本身)

  • writable: 属性是否可写

  • enumerable: 属性是否可枚举

  • configurable: 属性是否可配置

如下示例代码:

ts 复制代码
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log(target, propertyKey, descriptor);
}

class Calculator {
  @log
  add() {}
}

const calc = new Calculator();

最终输出结果如下图所示:

方法装饰器的使用

我们可以借助方法装饰器实现一个简单的日志装饰器,记录方法的调用:

typescript 复制代码
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  // 保存原始方法
  const originalMethod = descriptor.value;

  // 修改方法
  descriptor.value = function (...args: any[]) {
    console.log(`调用方法 ${propertyKey} 参数:`, args);

    // 调用原始方法并返回结果
    const result = originalMethod.apply(this, args);

    console.log(`方法 ${propertyKey} 返回:`, result);
    return result;
  };

  // 返回修改后的描述符
  return descriptor;
}

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

const calc = new Calculator();
calc.add(1, 2);
// 输出:
// 调用方法 add 参数: [1, 2]
// 方法 add 返回: 3

最终输出结果如下图所示:

我们还可以实现一个验证方法参数的装饰器:

typescript 复制代码
function validate(validator: (args: any[]) => boolean, errorMessage: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      if (!validator(args)) {
        throw new Error(`${propertyKey}: ${errorMessage}`);
      }

      return originalMethod.apply(this, args);
    };

    return descriptor;
  };
}

class UserService {
  @validate(
    (args) =>
      args.length > 0 && typeof args[0] === "string" && args[0].length > 0,
    "用户名必须是非空字符串"
  )
  createUser(username: string) {
    return `创建用户: ${username}`;
  }

  @validate(
    (args) =>
      args.length >= 2 &&
      typeof args[0] === "number" &&
      args[0] > 0 &&
      typeof args[1] === "string",
    "ID必须是正数,名称必须是字符串"
  )
  updateUser(id: number, name: string) {
    return `更新用户 ${id}: ${name}`;
  }
}

const userService = new UserService();
console.log(userService.createUser("Moment")); // 创建用户: Moment

try {
  userService.createUser(""); // 抛出错误
} catch (error: unknown) {
  if (error instanceof Error) {
    console.error(error.message); // createUser: 用户名必须是非空字符串
  } else {
    console.error("发生未知错误");
  }
}

try {
  userService.updateUser(-1, "Moment"); // 抛出错误
} catch (error: unknown) {
  if (error instanceof Error) {
    console.error(error.message); // updateUser: ID必须是正数,名称必须是字符串
  } else {
    console.error("发生未知错误");
  }
}

最终输出结果如下图所示:

当我们在方法装饰器中替换原始方法时,如果不正确处理 this 绑定,可能会导致以下问题:

ts 复制代码
function badDecorator(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  // 错误的实现 - 没有保留 this 上下文
  descriptor.value = function (...args: any[]) {
    console.log("方法执行前");
    // 直接调用原始方法,this 上下文丢失
    const result = originalMethod(...args);
    console.log("方法执行后");
    return result;
  };

  return descriptor;
}

在这个错误的实现中,当装饰后的方法被调用时,原始方法内的 this 不再指向类实例,而是指向全局对象或 undefined(严格模式下)。这会导致:

  1. 无法访问类的实例属性和方法

  2. 可能引发 "Cannot read property of undefined" 类型的错误

  3. 类中依赖 this 的代码无法正常工作

确保在调用原始方法时保留 this 上下文:

typescript 复制代码
function preserveContext(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    // 使用 apply 保留 this 上下文
    return originalMethod.apply(this, args);
  };

  return descriptor;
}

保留方法装饰器中的 this 上下文是确保装饰后的方法能够正确访问类实例属性和方法的关键。通过使用 apply 或 call 方法,我们可以确保原始方法在正确的上下文中执行,从而保持类的行为一致性和可预测性。

这是面向对象编程中的基本原则 - 对象的方法应该能够访问该对象的状态。在装饰器模式中,我们添加了新的行为,但不应该改变原有方法与对象之间的基本关系。

小结

方法装饰器是 TypeScript 中一种强大的元编程工具,它通过 @expression 语法应用于类的方法声明之前,接收目标对象、方法名和属性描述符作为参数。方法装饰器可以观察、修改或替换方法的定义,常用于实现横切关注点如日志记录、性能监控、权限验证等功能,而无需修改原始方法的核心逻辑。在实现装饰器时,必须正确保留方法的 this 上下文,以确保装饰后的方法能够正常访问类实例的属性和方法。

访问装饰器

访问器装饰器是 TypeScript 装饰器家族中的一员,专门用于装饰类中的访问器属性(getter 和 setter)。它允许你在不修改原始代码的情况下,拦截、修改或增强访问器的行为。

访问器装饰器声明在访问器声明之前,使用 @expression 形式,其中 expression 必须计算为一个函数,该函数在运行时被调用。

typescript 复制代码
class Example {
  private _value: string;

  constructor(value: string) {
    this._value = value;
  }

  @accessorDecorator
  get value(): string {
    return this._value;
  }

  set value(newValue: string) {
    this._value = newValue;
  }
}

访问器装饰器函数和方法装饰器一样,都是接收三个参数:

  1. target: 对于实例访问器,是类的原型对象;对于静态访问器,是类的构造函数

  2. propertyKey: 访问器的名称

  3. descriptor: 访问器的属性描述符(PropertyDescriptor)

typescript 复制代码
function accessorDecorator(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  console.log(target, propertyKey, descriptor);
  // 装饰器实现
}

属性描述符(PropertyDescriptor)对于访问器包含以下重要属性:

  • get: 获取属性值的函数

  • set: 设置属性值的函数

  • enumerable: 属性是否可枚举

  • configurable: 属性是否可配置

借助访问装饰器我们可以实现一个简单的日志装饰器,记录属性的获取和设置:

typescript 复制代码
function logAccess(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  // 保存原始的访问器
  const originalGet = descriptor.get;
  const originalSet = descriptor.set;

  // 修改 getter
  if (originalGet) {
    descriptor.get = function () {
      console.log(`获取 ${propertyKey} 的值`);
      return originalGet.call(this);
    };
  }

  // 修改 setter
  if (originalSet) {
    descriptor.set = function (value: any) {
      console.log(`设置 ${propertyKey} 的值为: ${value}`);
      originalSet.call(this, value);
    };
  }

  return descriptor;
}

class Person {
  private _name: string;

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

  @logAccess
  get name(): string {
    return this._name;
  }

  set name(value: string) {
    this._name = value;
  }
}

const person = new Person("张三");
console.log(person.name); // 输出: 获取 name 的值, 张三
person.name = "李四"; // 输出: 设置 name 的值为: 李四
console.log(person.name); // 输出: 获取 name 的值, 李四

如下输出结果所示:

我们还可以实现一个验证属性值的装饰器:

typescript 复制代码
function validateLength(min: number, max: number) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    // 检查 setter 是否存在
    const originalSet = descriptor.set;

    if (!originalSet) {
      throw new Error(`${propertyKey} 没有 setter 方法`);
    }

    // 修改 setter 添加验证
    descriptor.set = function (this: any, value: string) {
      if (value.length < min) {
        throw new Error(`${propertyKey} 长度不能小于 ${min}`);
      }

      if (value.length > max) {
        throw new Error(`${propertyKey} 长度不能大于 ${max}`);
      }

      // 调用原始的 setter
      originalSet.call(this, value);
    };

    return descriptor;
  };
}

class User {
  private _username: string;

  constructor(username: string) {
    this._username = username;
  }

  @validateLength(3, 20)
  get username(): string {
    return this._username;
  }

  set username(value: string) {
    this._username = value;
  }
}

const user = new User("admin");
console.log(user.username); // admin

try {
  user.username = "a"; // 抛出错误
} catch (error) {
  if (error instanceof Error) {
    console.error(error.message); // username 长度不能小于 3
  }
}

try {
  user.username = "a".repeat(30); // 抛出错误
} catch (error) {
  if (error instanceof Error) {
    console.error(error.message); // username 长度不能大于 20
  }
}

user.username = "moderator"; // 有效
console.log(user.username); // moderator

上面的输出结果如下图所示:

与方法装饰器类似,访问器装饰器也需要正确处理 this 上下文:

typescript 复制代码
function preserveContext(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalGet = descriptor.get;
  const originalSet = descriptor.set;

  if (originalGet) {
    descriptor.get = function () {
      // 使用 call 保留 this 上下文
      return originalGet.call(this);
    };
  }

  if (originalSet) {
    descriptor.set = function (value: any) {
      // 使用 call 保留 this 上下文
      originalSet.call(this, value);
    };
  }

  return descriptor;
}

保留访问器装饰器中的 this 上下文是确保装饰后的访问器能够正确访问类实例属性和方法的关键。通过使用 apply 或 call 方法,我们可以确保原始访问器在正确的上下文中执行,从而保持类的行为一致性和可预测性。

TypeScript 的限制

TypeScript 对访问器装饰器有一些限制:

  1. 访问器装饰器只能应用于访问器的第一个声明(getter 或 setter)

  2. 不能同时装饰 getter 和 setter,只能装饰其中一个

如下代码所示:

ts 复制代码
class Example {
  private _value: string;

  constructor(value: string) {
    this._value = value;
  }

  // 在 getter 上应用装饰器
  @logAccess
  get value(): string {
    return this._value;
  }

  // 错误:不能在 setter 上再次应用相同或不同的装饰器
  @logAccess // 这里会导致 ts(1207) 错误
  set value(newValue: string) {
    this._value = newValue;
  }
}

这个错误发生的原因是:

  1. 属性描述符的唯一性:在 JavaScript 中,一个属性(包括访问器属性)只有一个属性描述符

  2. 装饰器修改描述符:访问器装饰器修改的是整个属性的描述符,包括 getter 和 setter

  3. 冲突的修改:如果在 getter 和 setter 上都应用装饰器,这两个装饰器会尝试修改同一个描述符,导致冲突

如下警告:

正确的做法是只在第一个声明的访问器(通常是 getter)上应用装饰器:

ts 复制代码
class Example {
  private _value: string;

  constructor(value: string) {
    this._value = value;
  }

  // 正确:只在 getter 上应用装饰器
  @logAccess
  get value(): string {
    return this._value;
  }

  // 正确:不应用装饰器
  set value(newValue: string) {
    this._value = newValue;
  }
}

即使装饰器只应用于一个访问器,它仍然可以修改两个访问器的行为:

ts 复制代码
function logAccess(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
): PropertyDescriptor {
  // 保存原始的访问器
  const originalGet = descriptor.get;
  const originalSet = descriptor.set;

  // 修改 getter
  if (originalGet) {
    descriptor.get = function (this: any) {
      console.log(`获取 ${propertyKey} 的值`);
      return originalGet.call(this);
    };
  }

  // 修改 setter
  if (originalSet) {
    descriptor.set = function (this: any, value: any) {
      console.log(`设置 ${propertyKey} 的值为: ${value}`);
      originalSet.call(this, value);
    };
  }

  return descriptor;
}

小结

访问器装饰器是 TypeScript 中一种强大的元编程工具,它通过 @expression 语法应用于类的访问器声明之前,接收目标对象、属性名和属性描述符作为参数。访问器装饰器可以观察、修改或替换访问器的定义,常用于实现属性验证、日志记录、访问控制等功能,而无需修改原始访问器的核心逻辑。在实现装饰器时,必须正确保留访问器的 this 上下文,以确保装饰后的访问器能够正常访问类实例的属性和方法。

属性装饰器

属性装饰器是 TypeScript 装饰器家族中的一员,用于装饰类的属性(非方法)。它允许你在不修改原始代码的情况下,监控、修改或增强类属性的行为。

属性装饰器声明在属性声明之前,使用 @expression 形式,其中 expression 必须计算为一个函数,该函数在运行时被调用。

ts 复制代码
class Example {
  @propertyDecorator
  public name: string;

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

属性装饰器函数接收两个参数:

  1. target: 对于实例属性,是类的原型对象;对于静态属性,是类的构造函数

  2. propertyKey: 属性的名称

如下代码所示:

ts 复制代码
function propertyDecorator(target: any, propertyKey: string) {
  console.log(`装饰属性: ${propertyKey}`);
  // 装饰器实现
}

与方法装饰器和访问器装饰器不同,属性装饰器不接收属性描述符作为参数,这是因为在 TypeScript 中,属性声明没有关联的属性描述符。

接下来我们借助属性装饰器来实现一个简单的日志属性装饰器:

ts 复制代码
function logProperty(target: any, propertyKey: string) {
  // 获取属性的原始值
  let value = target[propertyKey];

  // 定义属性的 getter 和 setter
  Object.defineProperty(target, propertyKey, {
    get: function () {
      console.log(`获取 ${propertyKey} 的值: ${value}`);
      return value;
    },
    set: function (newValue) {
      console.log(`设置 ${propertyKey} 的值: ${newValue}`);
      value = newValue;
    },
    enumerable: true,
    configurable: true,
  });
}

class Person {
  @logProperty
  public name: string;

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

const person = new Person("张三");
console.log(person.name); // 输出: 获取 name 的值: 张三, 张三
person.name = "李四"; // 输出: 设置 name 的值: 李四
console.log(person.name); // 输出: 获取 name 的值: 李四, 李四

在上面的代码中,属性装饰器用于类的属性,主要用于添加元数据,无法直接修改属性的值,但可以通过 Object.defineProperty 实现控制。

最终的输出结果如下图所示:

小结

属性装饰器是 TypeScript 中的元编程工具,它在类定义时执行而非实例创建时,且装饰器执行时属性尚未初始化。虽然属性装饰器的返回值会被忽略,且不能直接修改属性行为(因缺少描述符参数),但可通过 Object.defineProperty 将属性转换为访问器或结合元数据来增强功能。这种装饰器特别适用于依赖注入、ORM 映射和表单验证等场景,使代码更加声明式和易于维护,是框架和库开发中不可或缺的工具。

参数装饰器

参数装饰器是 TypeScript 装饰器家族中较为特殊的一员,它应用于类构造函数或方法的参数声明。参数装饰器主要用于收集关于参数的元数据,通常与其他装饰器配合使用,特别是在依赖注入系统中。

参数装饰器声明在参数声明之前,使用 @expression 形式,其中 expression 必须计算为一个函数,该函数在运行时被调用。

ts 复制代码
class Example {
  greet(@parameterDecorator message: string) {
    console.log(message);
  }
}

参数装饰器函数接收三个参数:

  1. target: 对于实例方法的参数,是类的原型对象;对于静态方法的参数,是类的构造函数;对于构造函数的参数,是类本身

  2. methodName: 参数所属方法的名称(如果是构造函数参数,则为 "constructor")

  3. parameterIndex: 参数在函数参数列表中的索引(从 0 开始)

ts 复制代码
function parameterDecorator(
  target: any,
  methodName: string,
  parameterIndex: number
) {
  console.log(`装饰 ${methodName} 方法的第 ${parameterIndex} 个参数`);
  // 装饰器实现
}

参数装饰器的特点

  1. 返回值被忽略:参数装饰器的返回值会被忽略

  2. 无法修改参数行为:参数装饰器不能直接修改参数的行为

  3. 主要用于元数据:参数装饰器主要用于收集元数据,通常与反射元数据一起使用

  4. 执行时机:参数装饰器在类定义时执行,而不是在方法调用时执行

接下来我们借助参数装饰器实现一个简单的日志参数装饰器:

ts 复制代码
function logParameter(target: any, methodName: string, parameterIndex: number) {
  console.log(
    `类 ${target.constructor.name} 的方法 ${methodName} 的第 ${parameterIndex} 个参数被装饰`
  );
}

class Greeter {
  greet(@logParameter name: string, @logParameter age: number) {
    return `Hello, ${name}! You are ${age} years old.`;
  }

  static sayHi(@logParameter message: string) {
    console.log(message);
  }
}

// 调用 greet 方法
const greeter = new Greeter();
console.log(greeter.greet("Tom", 25));
// 输出:
// 类 Greeter 的方法 greet 的第 0 个参数被装饰
// 类 Greeter 的方法 greet 的第 1 个参数被装饰
// Hello, Tom! You are 25 years old.

// 调用静态方法 sayHi
Greeter.sayHi("Hi there!");
// 输出:
// 类 Greeter 的方法 sayHi 的第 0 个参数被装饰
// Hi there!

在上面的代码中,当你调用 greet 方法或 sayHi 静态方法时,装饰器 logParameter 会在参数传入之前被触发。它打印出每个参数的装饰信息,包括方法名、参数位置以及所在的类。然后方法会正常执行,输出最终的结果。对于 greet,它会打印 nameage 参数的装饰信息;对于静态方法 sayHi,则打印 message 参数的装饰信息。

小结

参数装饰器是 TypeScript 装饰器家族中较为特殊的一员,它主要用于收集关于参数的元数据,而不能直接修改参数的行为。参数装饰器通常与方法装饰器配合使用,在依赖注入、参数验证等场景中发挥重要作用。

依赖注入

依赖注入(Dependency Injection,简称 DI)是一种设计模式,它允许我们将一个类所依赖的对象(依赖项)从外部提供给它,而不是由类自己创建这些依赖项。这种模式遵循"控制反转"(Inversion of Control,IoC)原则,将对象的创建和管理责任从使用对象的代码转移到外部容器或框架。

依赖注入的核心概念主要有以下几个方面:

好的,下面是每个核心概念的 2-3 句话描述:

  1. 依赖(Dependency):依赖是指一个类或模块需要的外部资源或服务,通常是其他类或对象。例如,一个服务可能依赖于数据库连接或外部 API。依赖是类正常运行所必需的资源。

  2. 注入(Injection):注入是将外部依赖传递给目标类或对象的过程,通常通过构造函数、方法或属性进行。通过注入,目标类无需关心依赖的创建和管理,减少了类之间的耦合。

  3. 控制反转(Inversion of Control, IoC):控制反转是依赖注入的基础,它将类负责创建依赖的职责转移到外部容器。类只关注自己的逻辑,而容器负责提供和管理所有的依赖,增强了系统的灵活性和可扩展性。

  4. 容器(DI Container):容器是用于管理服务实例和它们的依赖关系的工具。它负责注册服务、解析依赖,并在需要时自动将依赖注入到目标类中,通常用于大型应用中来统一管理依赖。

  5. 松耦合(Loose Coupling):通过依赖注入,类之间不再直接依赖于彼此的实现,而是依赖于接口或抽象类。这种松耦合设计使得系统更容易扩展和维护,因为可以更容易地替换或修改依赖。

  6. 可替换性(Substitutability):依赖注入使得在运行时可以灵活替换服务的实现。例如,可以根据不同的环境或需求,替换数据库服务或日志记录服务,而不需要修改使用这些服务的类。

  7. 服务(Service):服务是提供特定功能的类或模块,通常用于处理业务逻辑、数据库访问等。服务作为依赖被注入到其他类中,减少了各类之间的耦合,便于复用和维护。

下面我们将使用 TypeScript 的各种装饰器(类装饰器、属性装饰器、方法装饰器和参数装饰器)来实现一个完整的依赖注入系统。

ts 复制代码
// 确保安装了 reflect-metadata 包
// npm install reflect-metadata

import "reflect-metadata";

// ==================== 1. 依赖注入容器 ====================

// 服务生命周期类型
enum Lifecycle {
  TRANSIENT, // 每次请求创建新实例
  SINGLETON, // 单例,全局共享一个实例
  SCOPED, // 作用域内共享一个实例(简化起见,本例不实现)
}

// 服务依赖信息
interface DependencyInfo {
  token: symbol | string;
}

// 服务注册信息
interface ServiceRegistration {
  token: symbol | string; // 服务标识
  type: any; // 服务类型
  lifecycle: Lifecycle; // 生命周期
  instance?: any; // 单例模式下的实例
  factory?: () => any; // 自定义工厂函数
  dependencies?: DependencyInfo[]; // 构造函数依赖
}

// 依赖注入容器
class DIContainer {
  private static instance: DIContainer;
  private services: Map<symbol | string, ServiceRegistration> = new Map();

  // 单例模式
  private constructor() {}

  static getInstance(): DIContainer {
    if (!DIContainer.instance) {
      DIContainer.instance = new DIContainer();
    }
    return DIContainer.instance;
  }

  // 注册服务
  register(
    token: symbol | string,
    type: any,
    lifecycle: Lifecycle = Lifecycle.SINGLETON
  ): void {
    // 获取构造函数的参数依赖
    const dependencies =
      (Reflect as any).getMetadata?.("di:dependencies", type) || [];

    this.services.set(token, {
      token,
      type,
      lifecycle,
      dependencies,
    });

    console.log(
      `服务注册: ${String(token)}, 生命周期: ${Lifecycle[lifecycle]}`
    );
  }

  // 注册自定义工厂
  registerFactory(
    token: symbol | string,
    factory: () => any,
    lifecycle: Lifecycle = Lifecycle.SINGLETON
  ): void {
    this.services.set(token, {
      token,
      type: Object,
      lifecycle,
      factory,
    });

    console.log(
      `工厂注册: ${String(token)}, 生命周期: ${Lifecycle[lifecycle]}`
    );
  }

  // 解析服务
  resolve<T>(token: symbol | string): T {
    const registration = this.services.get(token);

    if (!registration) {
      throw new Error(`服务未注册: ${String(token)}`);
    }

    // 单例模式,返回已存在的实例
    if (
      registration.lifecycle === Lifecycle.SINGLETON &&
      registration.instance
    ) {
      return registration.instance;
    }

    let instance: T;

    // 使用自定义工厂
    if (registration.factory) {
      instance = registration.factory() as T;
    } else {
      // 解析构造函数依赖
      const dependencies = (registration.dependencies || []).map(
        (dep: DependencyInfo) => {
          if (dep && dep.token) {
            return this.resolve(dep.token);
          }
          return undefined;
        }
      );

      // 创建实例
      instance = new registration.type(...dependencies) as T;
    }

    // 如果是单例,保存实例
    if (registration.lifecycle === Lifecycle.SINGLETON) {
      registration.instance = instance;
    }

    return instance;
  }
}

// ==================== 2. 装饰器 ====================

// 服务装饰器(类装饰器)
function Service(
  token: symbol | string = Symbol(),
  lifecycle: Lifecycle = Lifecycle.SINGLETON
) {
  return function <T extends { new (...args: any[]): {} }>(target: T) {
    // 使用类名作为默认标识,如果没有提供token
    const serviceToken = token || Symbol(target.name);

    // 注册服务
    const container = DIContainer.getInstance();
    container.register(serviceToken, target, lifecycle);

    // 保存标识,便于后续引用
    (Reflect as any).defineMetadata?.("di:token", serviceToken, target);

    return target;
  };
}

// 注入装饰器(属性装饰器)
function Inject(token: symbol | string = Symbol()) {
  return function (target: any, propertyKey: string) {
    // 获取属性类型
    const type = (Reflect as any).getMetadata?.(
      "design:type",
      target,
      propertyKey
    );
    // 使用类型作为默认标识
    const serviceToken =
      token ||
      (Reflect as any).getMetadata?.("di:token", type) ||
      Symbol(propertyKey);

    // 创建属性的 getter
    Object.defineProperty(target, propertyKey, {
      get: function () {
        // 延迟解析,在属性被访问时才获取服务实例
        const container = DIContainer.getInstance();
        return container.resolve(serviceToken);
      },
      enumerable: true,
      configurable: true,
    });

    console.log(
      `属性注入: ${target.constructor.name}.${propertyKey} <- ${String(
        serviceToken
      )}`
    );
  };
}

// 构造函数参数注入装饰器(参数装饰器)
function InjectParam(token: symbol | string = Symbol()) {
  return function (
    target: Object,
    methodName: string | symbol | undefined,
    parameterIndex: number
  ) {
    // 处理构造函数的情况
    const actualMethodName =
      methodName === undefined ? "constructor" : methodName;

    // 获取现有依赖
    const dependencies: DependencyInfo[] =
      (Reflect as any).getMetadata?.("di:dependencies", target) || [];

    // 设置依赖信息
    dependencies[parameterIndex] = { token };

    // 更新元数据
    (Reflect as any).defineMetadata?.("di:dependencies", dependencies, target);

    console.log(
      `参数注入: ${
        (target as any).constructor?.name || (target as any).name
      }.${String(actualMethodName)}[${parameterIndex}] <- ${String(token)}`
    );
  };
}

// 方法注入装饰器(方法装饰器)
function InjectMethod() {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      // 获取方法参数的注入信息
      const methodDependencies: DependencyInfo[] =
        (Reflect as any).getMetadata?.(
          "di:method:dependencies",
          target,
          propertyKey
        ) || [];

      // 解析依赖并替换参数
      const container = DIContainer.getInstance();
      const newArgs = [...args];

      for (let i = 0; i < methodDependencies.length; i++) {
        const dep = methodDependencies[i];
        if (dep && dep.token) {
          newArgs[i] = container.resolve(dep.token);
        }
      }

      // 调用原始方法
      return originalMethod.apply(this, newArgs);
    };

    return descriptor;
  };
}

// 方法参数注入装饰器(参数装饰器)
function InjectMethodParam(token: symbol | string = Symbol()) {
  return function (
    target: Object,
    methodName: string | symbol,
    parameterIndex: number
  ) {
    // 获取现有依赖
    const dependencies: DependencyInfo[] =
      (Reflect as any).getMetadata?.(
        "di:method:dependencies",
        target,
        methodName as string
      ) || [];

    // 设置依赖信息
    dependencies[parameterIndex] = { token };

    // 更新元数据
    (Reflect as any).defineMetadata?.(
      "di:method:dependencies",
      dependencies,
      target,
      methodName as string
    );

    console.log(
      `方法参数注入: ${(target as any).constructor.name}.${String(
        methodName
      )}[${parameterIndex}] <- ${String(token)}`
    );
  };
}

// ==================== 3. 示例服务 ====================

// 日志服务接口
interface ILogger {
  log(message: string): void;
  error(message: string): void;
}

// 服务标识符常量
const SERVICE_TOKENS = {
  LOGGER: Symbol("Logger"),
  CONFIG: Symbol("Config"),
  DATABASE: Symbol("Database"),
  USER_SERVICE: Symbol("UserService"),
  CURRENT_USER: Symbol("CurrentUser"),
};

// 日志服务实现
@Service(SERVICE_TOKENS.LOGGER)
class Logger implements ILogger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }

  error(message: string): void {
    console.error(`[ERROR] ${message}`);
  }
}

// 配置服务
@Service(SERVICE_TOKENS.CONFIG)
class ConfigService {
  private config: Record<string, any> = {
    apiUrl: "https://api.moment.com",
    timeout: 5000,
    maxRetries: 3,
  };

  get(key: string): any {
    return this.config[key];
  }

  set(key: string, value: any): void {
    this.config[key] = value;
  }
}

// 数据库服务
@Service(SERVICE_TOKENS.DATABASE)
class DatabaseService {
  constructor(
    @InjectParam(SERVICE_TOKENS.CONFIG) private config: ConfigService
  ) {
    console.log(`数据库服务初始化,API URL: ${config.get("apiUrl")}`);
  }

  query(sql: string): any[] {
    console.log(`执行查询: ${sql}`);
    return [
      { id: 1, name: "Item 1" },
      { id: 2, name: "Item 2" },
    ];
  }

  execute(sql: string): void {
    console.log(`执行命令: ${sql}`);
  }
}

// ==================== 4. 使用依赖注入的客户类 ====================

// 用户服务
@Service(SERVICE_TOKENS.USER_SERVICE)
class UserService {
  @Inject(SERVICE_TOKENS.LOGGER)
  private logger!: ILogger;

  @Inject(SERVICE_TOKENS.DATABASE)
  private db!: DatabaseService;

  constructor(
    @InjectParam(SERVICE_TOKENS.CONFIG) private config: ConfigService
  ) {
    console.log("用户服务初始化");
  }

  getUsers(): any[] {
    this.logger.log("获取用户列表");
    return this.db.query("SELECT * FROM users");
  }

  createUser(username: string, email: string): any {
    this.logger.log(`创建用户: ${username}, ${email}`);
    this.db.execute(
      `INSERT INTO users (username, email) VALUES ('${username}', '${email}')`
    );
    return { id: 3, username, email };
  }

  @InjectMethod()
  processUserData(
    data: any,
    @InjectMethodParam(SERVICE_TOKENS.LOGGER) logger?: ILogger
  ): void {
    logger?.log(`处理用户数据: ${JSON.stringify(data)}`);
    // 处理逻辑...
  }
}

// 应用控制器
class AppController {
  @Inject(SERVICE_TOKENS.USER_SERVICE)
  private userService!: UserService;

  @Inject(SERVICE_TOKENS.LOGGER)
  private logger!: ILogger;

  initialize(): void {
    this.logger.log("应用初始化");

    // 获取用户
    const users = this.userService.getUsers();
    console.log("用户列表:", users);

    // 创建用户
    const newUser = this.userService.createUser("john_doe", "john@example.com");
    console.log("新用户:", newUser);

    // 处理用户数据
    this.userService.processUserData({ name: "处理数据" });
  }
}

// ==================== 5. 运行示例 ====================

// 注册自定义服务
const container = DIContainer.getInstance();
container.registerFactory(
  SERVICE_TOKENS.CURRENT_USER,
  () => {
    return { id: 1, username: "admin", roles: ["ADMIN"] };
  },
  Lifecycle.SINGLETON
);

// 创建并初始化应用控制器
const appController = new AppController();
appController.initialize();

这个依赖注入系统使用 TypeScript 和 reflect-metadata 库,通过装饰器和容器模式实现服务的自动管理和依赖注入。容器 (DIContainer) 管理所有服务的注册和解析,支持三种生命周期:SINGLETON(单例)、TRANSIENT(每次创建新实例)和 SCOPED(在作用域内共享实例)。通过装饰器如 @Service@Inject@InjectParam 等,服务和依赖关系被标记并自动注入,避免了手动管理实例化和依赖关系的复杂性。

服务注册时,容器会根据元数据和反射机制提取构造函数的依赖关系,并解析它们。服务可以是自定义工厂生成的实例,或者是普通的类实例。依赖注入的实现包括属性注入、构造函数注入、方法参数注入等,通过反射机制动态解析和注入依赖。

这套系统使得服务的使用更加灵活,降低了服务之间的耦合性,便于扩展和测试。通过元数据反射和装饰器,开发者可以方便地管理复杂的依赖关系,确保代码的清晰和可维护性。

最终输出额结果如下图所示:

不同的装饰器执行顺序

接下来我们将来举使用一个简单的例子来讲解每个装饰器的执行顺序,如下代码所示:

ts 复制代码
// 类装饰器
function classDecorator(target: Function) {
  console.log("类装饰器执行:", target.name);
}

// 方法装饰器
function methodDecorator(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  console.log(`方法装饰器执行: ${propertyKey}`);
}

// 访问器装饰器
function accessorDecorator(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  console.log(`访问器装饰器执行: ${propertyKey}`);
}

// 属性装饰器
function propertyDecorator(target: any, propertyKey: string) {
  console.log(`属性装饰器执行: ${propertyKey}`);
}

// 参数装饰器
function parameterDecorator(
  target: any,
  methodName: string,
  parameterIndex: number
) {
  console.log(
    `参数装饰器执行: 方法 ${methodName} 的第 ${parameterIndex} 个参数`
  );
}

@classDecorator
class Example {
  @propertyDecorator
  public property: string;

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

  @methodDecorator
  greet(@parameterDecorator name: string, @parameterDecorator age: number) {
    console.log(`Hello, ${name}. You are ${age} years old.`);
  }

  @accessorDecorator
  get greetMessage() {
    return "Hello, world!";
  }
}

最终装饰器的执行顺序如下所示:

这个执行顺序的规律是:

  1. 属性装饰器优先执行

  2. 参数装饰器在其所属方法的装饰器之前执行,且从右到左

  3. 方法装饰器在其参数装饰器之后执行

  4. 访问器装饰器在方法装饰器之后执行

  5. 类装饰器最后执行

这与 TypeScript 装饰器的设计原则相符,确保成员(属性、参数、方法)的装饰器在类装饰器之前执行参数装饰器在其所属方法的装饰器之前执行,从内到外的执行顺序。

总结

装饰器是 TypeScript 提供的一种元编程机制,允许我们在类、方法、属性和参数上动态添加功能或元数据。通过装饰器,开发者可以在不修改原始代码的情况下,为目标对象增加额外的行为,例如依赖注入、日志记录、权限验证等。常见的装饰器有类装饰器、方法装饰器、属性装饰器、参数装饰器和访问器装饰器,它们在类的不同成员上发挥作用。装饰器通常与反射和元数据机制结合使用,提供更灵活的开发方式。执行顺序通常是属性装饰器优先,参数装饰器和方法装饰器按顺序执行,类装饰器最后执行。装饰器的应用可以显著提高代码的可读性、可维护性和复用性。

相关推荐
秋月华星22 分钟前
【flutter】TextField输入框工具栏文本为英文解决(不用安装插件版本
前端·javascript·flutter
—Qeyser1 小时前
用Deepseek写一个 HTML 和 JavaScript 实现一个简单的飞机游戏
javascript·游戏·html
千里码aicood1 小时前
[含文档+PPT+源码等]精品基于Python实现的校园小助手小程序的设计与实现
开发语言·前端·python
青红光硫化黑1 小时前
React基础之React.memo
前端·javascript·react.js
大麦大麦1 小时前
深入剖析 Sass:从基础到进阶的 CSS 预处理器应用指南
开发语言·前端·css·面试·rust·uni-app·sass
GDAL2 小时前
better-sqlite3之exec方法
javascript·sqlite
匹马夕阳3 小时前
基于Canvas和和原生JS实现俄罗斯方块小游戏
javascript·canva可画
m0_616188493 小时前
Vue3 中 Computed 用法
前端·javascript·vue.js
六个点3 小时前
图片懒加载与预加载的实现
前端·javascript·面试
weixin_460783873 小时前
Flutter解决TabBar顶部页面切换导致页面重载问题
android·javascript·flutter