JS `Decorator` (Stage 3) `Metadata` 规范与反射机制

各位观众老爷,大家好!今天咱们聊聊JavaScript装饰器和元数据,这俩哥们儿现在还是ES提案阶段,属于“预售房”,但已经足够性感了,值得咱们提前研究研究。

一、装饰器:给你的代码穿上“时装”

装饰器(Decorators)本质上就是一个函数,它可以用来修改类、方法、属性或参数的行为。它就像一个包装器,在不改变原始代码的情况下,给它增加额外的功能。想象一下,你有一辆普通的车,你想让它更酷炫,不用拆零件,直接贴个膜、加个尾翼,这就是装饰器的作用。

1. 装饰器的基本语法

装饰器使用@符号加上装饰器函数的名字来表示。它可以放在类、方法、属性或参数的前面。

@decorator
class MyClass {
  @decorator
  myMethod() {}

  @decorator
  myProperty = 123;

  constructor(@decorator myParam) {}
}

2. 类装饰器

类装饰器接收类的构造函数作为参数,可以用来修改类的行为。

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class BugReport {
  constructor(public id: number) {}
}

// 使用 Object.seal 可以防止对类或其原型进行添加、删除或重新配置属性的操作。

这个sealed装饰器可以阻止对BugReport类及其原型进行修改,相当于给类加了一道“金钟罩”。

3. 方法装饰器

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

  • target: 如果是静态成员,则为类的构造函数;如果是实例成员,则为类的原型对象。
  • propertyKey: 方法的名字。
  • descriptor: 属性描述符,类似于Object.getOwnPropertyDescriptor()的返回值。
function log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with arguments: ${JSON.stringify(args)}`);
    const result = originalMethod.apply(this, args);
    console.log(`Method ${propertyKey} returned: ${result}`);
    return result;
  };

  return descriptor;
}

class Calculator {
  @log
  add(x: number, y: number): number {
    return x + y;
  }
}

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

这个log装饰器给add方法增加了日志功能,在方法调用前后打印相关信息,方便调试。

4. 属性装饰器

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

  • target: 如果是静态成员,则为类的构造函数;如果是实例成员,则为类的原型对象。
  • propertyKey: 属性的名字。
function readonly(target: Object, propertyKey: string) {
  Object.defineProperty(target, propertyKey, {
    writable: false,
  });
}

class Person {
  @readonly
  name: string = "John Doe";

  constructor() {
    // this.name = "Jane Doe"; // 报错:Cannot assign to read only property 'name' of object
  }
}

const person = new Person();
console.log(person.name); // John Doe
// person.name = "Jane Doe"; // 报错:Cannot assign to read only property 'name' of object

readonly装饰器让name属性变为只读,尝试修改会报错。

5. 参数装饰器

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

  • target: 如果是静态成员,则为类的构造函数;如果是实例成员,则为类的原型对象。
  • propertyKey: 方法的名字。
  • parameterIndex: 参数在参数列表中的索引。
function required(target: Object, propertyKey: string, parameterIndex: number) {
  // 在这里你可以存储参数信息,稍后在方法调用时进行验证
  console.log(`Required decorator applied to parameter at index ${parameterIndex} of method ${propertyKey}`);
}

class Greeter {
  greeting: string;

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

  greet(@required name: string) {
    return "Hello, " + name + ", " + this.greeting;
  }
}

required装饰器可以用来标记必需的参数,但它本身并没有验证功能,你需要自己实现验证逻辑。通常,你会将参数信息存储起来,然后在方法调用时进行验证。

二、元数据:给你的代码贴上“标签”

元数据(Metadata)就是关于数据的数据。它描述了数据的特征、属性和关系。在JavaScript中,我们可以使用元数据来存储关于类、方法、属性或参数的额外信息。这些信息可以被其他代码读取和使用,从而实现更高级的功能。

1. reflect-metadata

要使用元数据,我们需要引入reflect-metadata库。这个库提供了一组API来操作元数据。

npm install reflect-metadata --save

然后在你的代码中导入它:

import "reflect-metadata";

2. Reflect.defineMetadataReflect.getMetadata

Reflect.defineMetadata用于定义元数据,Reflect.getMetadata用于获取元数据。

  • Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey?): 为目标对象(类、方法、属性)定义元数据。
    • metadataKey: 元数据的键。
    • metadataValue: 元数据的值。
    • target: 目标对象。
    • propertyKey: (可选) 如果目标是方法或属性,则指定方法或属性的名字。
  • Reflect.getMetadata(metadataKey, target, propertyKey?): 获取目标对象的元数据。
    • metadataKey: 元数据的键。
    • target: 目标对象。
    • propertyKey: (可选) 如果目标是方法或属性,则指定方法或属性的名字。

3. 使用元数据

import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
  return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
  return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

class Greeter {
  @format("Hello, %s")
  greeting: string;

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

  greet(name: string) {
    let formatString = getFormat(this, "greeting");
    return formatString.replace("%s", name) + ", " + this.greeting;
  }
}

let greeter = new Greeter("world!");
console.log(greeter.greet("Bob")); // Hello, Bob, world!

在这个例子中,format装饰器使用Reflect.defineMetadatagreeting属性定义了元数据,getFormat函数使用Reflect.getMetadata获取了元数据,并在greet方法中使用。

4. 元数据和装饰器结合

元数据和装饰器通常一起使用,装饰器负责定义元数据,然后在其他地方读取和使用这些元数据。

import "reflect-metadata";

const typeMetadataKey = Symbol("type");

function type(type: any) {
  return Reflect.metadata(typeMetadataKey, type);
}

function getType(target: any, propertyKey: string): any {
  return Reflect.getMetadata(typeMetadataKey, target, propertyKey);
}

class Point {
  x: number;
  y: number;
}

class Line {
  @type(Point)
  start: Point;

  @type(Point)
  end: Point;
}

console.log(getType(Line.prototype, "start")); // [Function: Point]

type装饰器使用Reflect.defineMetadatastart属性定义了类型信息,getType函数使用Reflect.getMetadata获取了类型信息。

三、装饰器和元数据的实际应用

装饰器和元数据有很多实际应用场景,例如:

  • 依赖注入 (Dependency Injection): 使用装饰器和元数据来标记依赖关系,实现依赖注入容器。
  • 路由 (Routing): 使用装饰器来定义路由,将URL映射到处理函数。
  • 验证 (Validation): 使用装饰器来定义验证规则,验证数据的有效性。
  • 序列化 (Serialization): 使用装饰器来定义序列化规则,将对象转换为JSON或其他格式。
  • AOP (Aspect-Oriented Programming): 使用装饰器来实现横切关注点,例如日志、权限控制等。
  • ORM (Object-Relational Mapping): 使用装饰器和元数据来定义数据模型,将对象映射到数据库表。

四、一个更复杂的例子:验证器

咱们来撸一个稍微复杂点的例子,实现一个简单的验证器。

import "reflect-metadata";

const validationMetadataKey = Symbol("validation");

interface ValidationOptions {
  required?: boolean;
  minLength?: number;
  maxLength?: number;
  pattern?: RegExp;
}

function validate(options: ValidationOptions) {
  return function (target: any, propertyKey: string) {
    Reflect.defineMetadata(validationMetadataKey, options, target, propertyKey);
  };
}

function getValidationOptions(target: any, propertyKey: string): ValidationOptions | undefined {
  return Reflect.getMetadata(validationMetadataKey, target, propertyKey);
}

function isValid(obj: any): boolean {
  const prototype = Object.getPrototypeOf(obj);
  const propertyKeys = Object.getOwnPropertyNames(prototype);

  for (const propertyKey of propertyKeys) {
    if (propertyKey === "constructor") {
      continue;
    }

    const options = getValidationOptions(prototype, propertyKey);
    if (!options) {
      continue;
    }

    const value = obj[propertyKey];

    if (options.required && (value === null || value === undefined || value === "")) {
      console.error(`Property ${propertyKey} is required.`);
      return false;
    }

    if (options.minLength !== undefined && typeof value === "string" && value.length < options.minLength) {
      console.error(`Property ${propertyKey} must be at least ${options.minLength} characters long.`);
      return false;
    }

    if (options.maxLength !== undefined && typeof value === "string" && value.length > options.maxLength) {
      console.error(`Property ${propertyKey} must be at most ${options.maxLength} characters long.`);
      return false;
    }

    if (options.pattern && typeof value === "string" && !options.pattern.test(value)) {
      console.error(`Property ${propertyKey} does not match the required pattern.`);
      return false;
    }
  }

  return true;
}

class User {
  @validate({ required: true, minLength: 3, maxLength: 20 })
  username: string;

  @validate({ required: true, pattern: /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,6}$/ })
  email: string;

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

const validUser = new User("johndoe", "[email protected]");
const invalidUser1 = new User("", "[email protected]");
const invalidUser2 = new User("jd", "[email protected]");
const invalidUser3 = new User("johndoe", "invalid-email");

console.log("Valid user:", isValid(validUser)); // Valid user: true
console.log("Invalid user 1:", isValid(invalidUser1)); // Invalid user 1: false (username required)
console.log("Invalid user 2:", isValid(invalidUser2)); // Invalid user 2: false (username too short)
console.log("Invalid user 3:", isValid(invalidUser3)); // Invalid user 3: false (invalid email)

这个例子定义了一个validate装饰器,它可以用来指定属性的验证规则。isValid函数会遍历对象的属性,读取验证规则,并进行验证。

五、装饰器和元数据的注意事项

  • 性能: 过度使用装饰器和元数据可能会影响性能,因为它们需要在运行时进行处理。
  • 可读性: 过多的装饰器可能会降低代码的可读性,所以要适度使用。
  • 类型安全: 虽然装饰器可以增强代码的功能,但它们也可能引入类型错误,所以要仔细测试。
  • 兼容性: 装饰器和元数据目前还是ES提案,可能在不同的JavaScript引擎中有不同的实现,所以要注意兼容性。
  • 运行时依赖: 使用 reflect-metadata 需要在运行时引入这个库,这会增加你的项目体积。

六、总结

装饰器和元数据是强大的工具,可以用来增强JavaScript代码的功能和可维护性。虽然它们目前还是ES提案,但已经有很多库和框架在使用它们。掌握装饰器和元数据,可以让你写出更优雅、更灵活的代码。

特性 描述 应用场景
装饰器 本质上是一个函数,用于修改类、方法、属性或参数的行为。 依赖注入、路由、验证、AOP、ORM
元数据 描述数据的数据,用于存储关于类、方法、属性或参数的额外信息。 类型信息、验证规则、序列化规则
reflect-metadata 提供了一组API来操作元数据,包括定义和获取元数据。 所有需要使用元数据的场景,配合装饰器使用,例如依赖注入、验证、序列化等。
注意事项 性能影响、可读性降低、类型安全问题、兼容性问题、运行时依赖。 使用时需要权衡利弊,避免过度使用,注意代码的可读性和可维护性,并进行充分的测试。

好了,今天的讲座就到这里。希望大家有所收获,早日用上装饰器和元数据,让自己的代码更上一层楼! 记得点赞,收藏,下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注