从零开始Node之旅——装饰器

在 Node.js 里,装饰器是一项极为实用的语法糖,它能够对类、方法、属性等进行元编程,从而在不改变原有代码结构的前提下,为其添加额外功能。
装饰器本质上是一种特殊的函数,其返回值是一个用于修改类、方法、属性等目标对象的函数。借助@装饰器名这种语法,我们可以将其应用到目标对象上。

类装饰器

类装饰器用于修改类的定义,它接收构造函数作为参数。下面是一个类装饰器的示例:

javascript 复制代码
function logClass(constructor: Function) {
  console.log(`Class ${constructor.name} created`);
  // 可以通过修改 prototype 来添加新的属性或方法
  constructor.prototype.log = function() {
    console.log(`Instance of ${constructor.name}`);
  };
}

@logClass
class MyClass {
  constructor() {}
}

const instance = new MyClass();
// 可以调用装饰器添加的方法
(instance as any).log(); // 输出 "Instance of MyClass"

方法装饰器

方法装饰器用于修改类的方法,它接收三个参数:目标对象、方法名和方法描述符。以下是方法装饰器的示例:

typescript 复制代码
function logMethod(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`Calling method ${propertyKey} with args: ${JSON.stringify(args)}`);
    const result = originalMethod.apply(this, args);
    console.log(`Method ${propertyKey} returned: ${result}`);
    return result;
  };
  return descriptor;
}

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

const calc = new Calculator();
calc.add(1, 2); 
// 输出:
// Calling method add with args: [1,2]
// Method add returned: 3

在方法装饰器中,三个参数分别是目标对象(target)属性名(propertyKey)属性描述符(descriptor) 。这三个参数构成了装饰器修改类方法的基础,下面详细解释它们的概念和用途。

1. 目标对象(Target)

  • 概念

    目标对象是类的原型(prototype) (对于实例方法)或类的构造函数本身(对于静态方法)。它是装饰器应用的对象,通过它可以访问和修改类的原型链。

  • 作用

    • 修改类的原型,添加新属性或方法。
    • 获取或设置类的现有属性。
    • 实现依赖注入等功能。

示例:实例方法 vs 静态方法

less 复制代码
class Example {
  // 实例方法装饰器:target 是 Example.prototype
  @log
  instanceMethod() {}

  // 静态方法装饰器:target 是 Example 构造函数
  @log
  static staticMethod() {}
}

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log('Target:', target === Example.prototype ? 'Prototype' : 'Constructor');
}

2. 属性名(PropertyKey)

  • 概念

    属性名是一个字符串或符号(Symbol) ,表示被装饰方法的名称。通过它可以明确知道装饰器应用于哪个方法。

  • 作用

    • 在运行时动态获取方法名称。
    • 结合反射 API(如Reflect.metadata)存储或读取元数据。

示例:记录方法名称

typescript 复制代码
function methodNameLogger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log(`Method "${propertyKey}" was decorated`);
  // 输出:"Method "greet" was decorated"
}

class Greeter {
  @methodNameLogger
  greet(name: string) {
    return `Hello, ${name}`;
  }
}

3. 属性描述符(PropertyDescriptor)

  • 概念

    属性描述符是一个对象 ,定义了方法的配置信息,与Object.defineProperty()中的描述符格式一致。它包含以下属性:

    • value:方法的实现(函数体)。
    • writable:是否可修改方法实现。
    • enumerable:是否可枚举(如for...in循环)。
    • configurable:是否可删除或重新定义。
    • get/set:存取器属性(如果是访问器方法)。
  • 作用

    • 修改方法行为 :通过重写descriptor.value来拦截方法调用。
    • 配置方法特性:控制方法的可写性、可枚举性等。
    • 实现访问控制 :通过getter/setter实现属性的访问拦截。

示例:修改方法描述符

typescript 复制代码
function readonly(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  // 禁止修改方法实现
  descriptor.writable = false;
  
  // 可选:修改方法行为
  const originalMethod = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`Readonly method "${propertyKey}" called`);
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

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

const calc = new Calculator();
calc.add = () => 0; // TypeError: Cannot assign to read only property 'add'

属性装饰器

属性装饰器用于修改类的属性,它接收目标对象和属性名作为参数。示例如下:

ini 复制代码
function uppercase(
  target: any,
  propertyKey: string
) {
  let value = target[propertyKey];
  const getter = () => value;
  const setter = (newVal: string) => {
    value = newVal.toUpperCase();
  };
  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class Person {
  @uppercase
  name: string;

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

const person = new Person("john");
console.log(person.name); // 输出 "JOHN"
person.name = "doe";
console.log(person.name); // 输出 "DOE"

参数装饰器

参数装饰器用于获取方法参数的元数据,它接收目标对象、方法名和参数索引作为参数。示例如下:

typescript 复制代码
import 'reflect-metadata';

// 元数据键
const METADATA_KEY = 'validation:rules';

// 参数验证装饰器
function Validate(rule: (value: any) => boolean) {
  return function(target: any, propertyKey: string, parameterIndex: number) {
    // 获取现有规则或初始化空数组
    const rules: { index: number; rule: (value: any) => boolean }[] = 
      Reflect.getMetadata(METADATA_KEY, target, propertyKey) || [];
    
    // 添加新规则
    rules.push({ index: parameterIndex, rule });
    
    // 存储规则元数据
    Reflect.defineMetadata(METADATA_KEY, rules, target, propertyKey);
  };
}

// 方法拦截器:验证参数
function validateParams(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    // 获取参数验证规则
    const rules: { index: number; rule: (value: any) => boolean }[] = 
      Reflect.getMetadata(METADATA_KEY, target, propertyKey) || [];
    
    // 验证每个参数
    for (const { index, rule } of rules) {
      if (!rule(args[index])) {
        throw new Error(`Validation failed for parameter at index ${index} of method "${propertyKey}"`);
      }
    }
    
    // 执行原方法
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

class UserService {
  @validateParams
  createUser(
    @Validate(name => typeof name === 'string' && name.length > 0) name: string,
    @Validate(age => typeof age === 'number' && age > 0) age: number
  ) {
    return { name, age };
  }
}

// 使用示例
const service = new UserService();
service.createUser('Alice', 30); // 正常执行
service.createUser('', 0); // 抛出错误
相关推荐
bobz9651 小时前
vxlan 为什么一定要封装在 udp 报文里?
后端
bobz9651 小时前
vxlan 直接使用 ip 层封装是否可以?
后端
郑道3 小时前
Docker 在 macOS 下的安装与 Gitea 部署经验总结
后端
3Katrina3 小时前
妈妈再也不用担心我的课设了---Vibe Coding帮你实现期末课设!
前端·后端·设计
汪子熙3 小时前
HSQLDB 数据库锁获取失败深度解析
数据库·后端
高松燈3 小时前
若伊项目学习 后端分页源码分析
后端·架构
没逻辑4 小时前
主流消息队列模型与选型对比(RabbitMQ / Kafka / RocketMQ)
后端·消息队列
倚栏听风雨4 小时前
SwingUtilities.invokeLater 详解
后端
Java中文社群4 小时前
AI实战:一键生成数字人视频!
java·人工智能·后端
王中阳Go5 小时前
从超市收银到航空调度:贪心算法如何破解生活中的最优决策谜题?
java·后端·算法