TypeScript 中的装饰器(Decorators)与元数据反射

好的,各位观众老爷们,欢迎来到《TypeScript 魔法学院》!我是你们今天的讲师——代码界的哈利·波特(咳咳,稍微夸张了点)。今天我们要一起探索 TypeScript 中两个非常酷炫的魔法:装饰器(Decorators)和元数据反射(Metadata Reflection)。

准备好了吗?拿起你的魔杖(键盘),让我们开始这场奇妙的旅程吧!🧙‍♂️

第一章:装饰器——给你的代码穿上华丽的礼服

想象一下,你正在参加一个盛大的舞会。你精心打扮了一番,穿上了最漂亮的礼服,瞬间成为了全场的焦点。装饰器就像这件礼服,它可以让你在不改变原有代码结构的情况下,给你的类、方法、属性等“穿”上额外的功能。

什么是装饰器?

简单来说,装饰器就是一个函数,它可以用来修改类、方法、属性或参数的行为。它使用 @ 符号作为前缀,放在要装饰的目标前面。

@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}

在这个例子中,@sealed 就是一个装饰器,它可能会阻止 Greeter 类被继承(具体实现我们稍后会讲到)。

装饰器的种类

TypeScript 中的装饰器主要有四种:

  • 类装饰器(Class Decorators): 装饰类本身。
  • 方法装饰器(Method Decorators): 装饰类中的方法。
  • 属性装饰器(Property Decorators): 装饰类中的属性。
  • 参数装饰器(Parameter Decorators): 装饰方法的参数。

每种装饰器接收的参数不同,功能也各异。

类装饰器:让你的类坚不可摧

类装饰器接收一个参数:类的构造函数。我们可以利用它来修改类的行为,比如阻止继承、添加静态属性等。

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

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

// 尝试继承 BugReport 类会报错
// class Report extends BugReport {} // 错误:无法从“BugReport”扩展,因为它已被密封。

在这个例子中,@sealed 装饰器会使用 Object.seal 方法来阻止 BugReport 类及其原型被修改或继承,让你的类坚不可摧,就像一个金钟罩!🛡️

方法装饰器:给你的方法加点“料”

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

  • target:如果是静态成员,则是类的构造函数;如果是实例成员,则是类的原型对象。
  • propertyKey:方法的名字。
  • descriptor:属性描述符,包含方法的 valuewritableenumerableconfigurable 等属性。

我们可以利用方法装饰器来修改方法的行为,比如记录方法的调用时间、验证方法的参数等。

function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const startTime = Date.now();
    const result = originalMethod.apply(this, args);
    const endTime = Date.now();
    console.log(`Method ${propertyKey} took ${endTime - startTime}ms to execute.`);
    return result;
  };

  return descriptor;
}

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

const calculator = new Calculator();
calculator.add(1, 2); // 输出:Method add took 0ms to execute.

在这个例子中,@logMethod 装饰器会记录 add 方法的执行时间,并在控制台输出。这样,你就可以轻松地监控你的方法的性能,就像给你的方法装上了一个“计时器”! ⏱️

属性装饰器:让你的属性更“智能”

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

  • target:如果是静态成员,则是类的构造函数;如果是实例成员,则是类的原型对象。
  • propertyKey:属性的名字。

我们可以利用属性装饰器来修改属性的行为,比如验证属性的值、延迟加载属性等。

function readonly(target: any, propertyKey: string) {
  Object.defineProperty(target, propertyKey, {
    writable: false,
  });
}

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

const person = new Person();
// person.name = "Jane Doe"; // 错误:无法分配到“name”,因为它是只读属性。

在这个例子中,@readonly 装饰器会将 name 属性设置为只读,防止被修改,就像给你的属性加上了一把“锁”! 🔒

参数装饰器:给你的参数加上“标签”

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

  • target:如果是静态成员,则是类的构造函数;如果是实例成员,则是类的原型对象。
  • propertyKey:方法的名字。
  • parameterIndex:参数在参数列表中的索引。

我们可以利用参数装饰器来给参数加上一些“标签”,以便在运行时获取参数的信息。

function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
  console.log(`Required parameter at index ${parameterIndex} for method ${String(propertyKey)}`);
}

class Form {
  submit(@required name: string, @required email: string) {
    console.log(`Submitting form with name: ${name}, email: ${email}`);
  }
}

const form = new Form(); // 输出:Required parameter at index 0 for method submit
                           //       Required parameter at index 1 for method submit

在这个例子中,@required 装饰器会在控制台输出参数的索引和方法的名字,让你知道哪些参数是必须的,就像给你的参数贴上了“重要”标签! 🏷️

装饰器工厂:创造你自己的魔法

有时候,我们希望装饰器能够接收一些参数,以便定制其行为。这时,我们可以使用装饰器工厂。

装饰器工厂就是一个返回装饰器函数的函数。

function log(message: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      console.log(message);
      return originalMethod.apply(this, args);
    };

    return descriptor;
  };
}

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

  @log("Calling greet method...")
  greet() {
    return "Hello, " + this.greeting;
  }
}

const greeter = new Greeter("world");
greeter.greet(); // 输出:Calling greet method...
                  //       Hello, world

在这个例子中,log 函数就是一个装饰器工厂,它接收一个 message 参数,并返回一个装饰器函数。这样,我们就可以根据不同的需求,创建不同的装饰器,就像一个魔法制造工厂! 🏭

第二章:元数据反射——窥探代码的“灵魂”

想象一下,你拥有一台可以看穿物体内部结构的 X 光机。元数据反射就像这台 X 光机,它可以让你在运行时获取类、方法、属性等的元数据信息。

什么是元数据?

元数据就是描述数据的数据。在 TypeScript 中,元数据可以包含类、方法、属性的类型信息、装饰器信息等。

什么是元数据反射?

元数据反射就是一种在运行时获取元数据信息的技术。TypeScript 并没有内置元数据反射 API,但我们可以使用 reflect-metadata 库来实现。

安装 reflect-metadata

npm install reflect-metadata --save

启用元数据反射

在使用元数据反射之前,我们需要在 TypeScript 配置文件 tsconfig.json 中启用 emitDecoratorMetadata 选项。

{
  "compilerOptions": {
    "target": "es5",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "module": "commonjs",
    "moduleResolution": "node"
  }
}

使用元数据反射

安装并启用元数据反射后,我们就可以使用 Reflect 对象来获取元数据信息了。

import 'reflect-metadata';

function logType(target: any, propertyKey: string) {
  const type = Reflect.getMetadata("design:type", target, propertyKey);
  console.log(`${propertyKey} type: ${type.name}`);
}

class Demo {
  @logType
  name: string;

  @logType
  age: number;
}

// 输出:name type: String
//       age type: Number

在这个例子中,@logType 装饰器使用 Reflect.getMetadata 方法来获取 nameage 属性的类型信息,并在控制台输出。

常用的元数据键

reflect-metadata 库定义了一些常用的元数据键:

  • design:type:属性的类型。
  • design:paramtypes:方法参数的类型。
  • design:returntype:方法的返回类型。

元数据反射的应用场景

元数据反射在很多场景下都非常有用,比如:

  • 依赖注入: 可以根据参数的类型自动注入依赖。
  • 对象关系映射(ORM): 可以根据类的属性自动映射到数据库表。
  • 序列化与反序列化: 可以根据类的属性类型自动进行序列化和反序列化。
  • 表单验证: 可以根据属性的类型和装饰器信息自动生成验证规则。

第三章:装饰器与元数据反射的完美结合

装饰器和元数据反射就像一对黄金搭档,它们可以一起实现更强大的功能。

依赖注入

我们可以使用装饰器和元数据反射来实现依赖注入,让代码更加灵活和可维护。

import 'reflect-metadata';

const Injectable = (): ClassDecorator => {
    return (target: any) => {
        Reflect.defineMetadata('design:injectable', true, target);
    };
};

const Inject = (token: any): ParameterDecorator => {
    return (target: any, propertyKey: string | symbol | undefined, parameterIndex: number) => {
        Reflect.defineMetadata(token, parameterIndex, target, propertyKey);
    };
};

const resolveDependencies = (target: any) => {
    const params = Reflect.getMetadata('design:paramtypes', target);
    if (!params) {
        return new target();
    }

    const injections = params.map((param: any, index: number) => {
        const token = Object.getOwnPropertySymbols(target.prototype).find(symbol => {
            return Reflect.getMetadata(symbol, target.prototype) === index;
        });

        if (token) {
            return resolveDependencies(param); // 递归解析依赖
        }

        return new param();
    });

    return new target(...injections);
};

@Injectable()
class Logger {
    log(message: string) {
        console.log(`Logger: ${message}`);
    }
}

const LOGGER = Symbol('LOGGER');

@Injectable()
class UserService {
    constructor(@Inject(LOGGER) private logger: Logger) {}

    createUser(name: string) {
        this.logger.log(`Creating user: ${name}`);
    }
}

// 创建 Logger 实例
const logger = new Logger();
Reflect.defineMetadata(LOGGER, 0, UserService.prototype); // 将 Logger 实例与 UserService 的第一个参数关联

// 解析 UserService 的依赖并创建实例
const userService = resolveDependencies(UserService);
userService.createUser('Alice'); // 输出:Logger: Creating user: Alice

在这个例子中,我们定义了 @Injectable@Inject 装饰器,分别用于标记可注入的类和注入的依赖。resolveDependencies 函数使用元数据反射来解析类的依赖,并自动创建依赖的实例。

对象关系映射(ORM)

我们可以使用装饰器和元数据反射来实现 ORM,将类映射到数据库表,简化数据库操作。

(由于篇幅限制,这里只给出思路,具体实现比较复杂,需要编写更多的代码。)

  1. 定义装饰器来标记类的属性和数据库表的字段之间的映射关系。
  2. 使用元数据反射来获取类的属性类型和映射信息。
  3. 编写 ORM 引擎,根据类的属性类型和映射信息自动生成 SQL 语句,并执行数据库操作。

总结

装饰器和元数据反射是 TypeScript 中两个非常强大的特性,它们可以让你在不改变原有代码结构的情况下,给你的代码添加额外的功能,并窥探代码的“灵魂”。掌握它们,你就可以编写出更加灵活、可维护、可扩展的代码,成为真正的 TypeScript 魔法师! 🧙‍♀️

一些建议

  • 不要滥用装饰器和元数据反射,只在必要的时候使用它们。
  • 编写清晰、简洁的装饰器代码,避免过度复杂的逻辑。
  • 充分利用元数据反射的优势,简化代码的实现。
  • 持续学习和实践,掌握更多的 TypeScript 魔法!

希望今天的讲座对大家有所帮助。记住,代码的世界充满着无限可能,只要你敢于探索,就一定能发现更多的惊喜!下次再见! 👋

发表回复

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