TypeScript装饰器:元编程的利器

引言

在 TypeScript 中,装饰器是一种元编程的工具。TypeScript 装饰器是其最为强大的元编程特性之一,可以添加元数据(metadata)和行为(behavior)到类、方法、属性和参数上,它可以在不改变代码逻辑的情况下,对代码结构进行注解、扩展、修改和重构,使得代码更加优雅、简洁、可维护和易于扩展。它是一种相对新的语言特性,但已经被广泛使用于不同的框架和库中,例如 Angular 、NestJS 、Express 等。

在本文中,我们将探讨 TypeScript 装饰器的特性和使用场景。

装饰器的介绍

装饰器是一种特殊的 函数 ,它可以用来修饰 属性方法参数 ,通过在这些目标上添加装饰器来 改变它们的行为 。它的语法类似于注释,但它真正的作用是 在代码编译阶段进行元编程操作

装饰器的基本语法

如下所示:

@decorator
class MyClass {
    
    
    // class implementation
}

@decorator
function MyFunction() {
    
    
    // function implementation
}

@decorator
property MyProperty {
    
    
    // property implementation
}

@decorator
method MyMethod() {
    
    
    // method implementation
}

@decorator
parameter MyParameter {
    
    
    // parameter implementation
}
  • 在 TypeScript 中,装饰器是一个函数或者一个表达式,它可以接收一个或者多个参数,这些参数取决于装饰器被应用的上下文。

为类添加元数据

  • 下面是一个简单的装饰器例子,它为一个类添加了一个元数据:

    function MyClassDecorator(target: Function) {
          
          
        Reflect.defineMetadata("custom:my-metadata", "Hello, world!", target);
    }
    
    @MyClassDecorator
    class MyClass {
          
          
        // class implementation
    }
    
  • 在这个装饰器中,我们使用了 TypeScript 的反射 API Reflect 来添加一个元数据。这个元数据的key是" custom:my-metadata “,value 是” Hello, world! "。装饰器被应用在 MyClass 上,所以 MyClassDecorator 被调用时,它的参数 target 就是 MyClass 这个函数。我们可以将这个元数据用于后续的操作,例如序列化和反序列化。下面是如何获取这个元数据:

    const metadata = Reflect.getMetadata("custom:my-metadata", MyClass); console.log(metadata); // Output: "Hello, world!"
    
  • 在这个例子中,我们使用了 ReflectgetMetadata 方法来获取元数据。这个方法的第一个参数是元数据的 key ,第二个参数是定义这个元数据的函数。

添加行为

  • 除了添加元数据,装饰器还可以添加行为。下面是一个添加行为的例子,它为一个类的属性添加了 setter 和 getter :
    function MyPropertyDecorator(target: any, propertyKey: string) {
          
          
        // property getter
        const getter = function () {
          
          
            console.log(`Getting value for ${
            
            propertyKey}`);
            return this[`_${
            
            propertyKey}`];
        };
        // property setter
        const setter = function (newVal) {
          
          
            console.log(`Setting value for ${
            
            propertyKey}`);
            this[`_${
            
            propertyKey}`] = newVal;
        };
        // delete and redefine the property
        if (delete this[propertyKey]) {
          
          
            Object.defineProperty(target, propertyKey, {
          
          
                get: getter,
                set: setter,
                enumerable: true,
                configurable: true
            });
        }
    }
    
    class MyClass {
          
          
        @MyPropertyDecorator
        myProp: string;
    }
    
  • 在这个例子中,我们定义了一个名为 MyPropertyDecorator 的装饰器,它被应用在 myProp 属性上。当装饰器被应用时,它重新定义了这个属性的 getter 和 setter 。在这个例子中,我们可以使用 myProp 属性的 getter 和 setter 来记录 get 和 set 的行为。

TypeScript装饰器的使用场景

  • 日志记录
    • 装饰器可以记录日志,以便开发者了解程序的运行情况,便于调试和优化程序。
      function log(target: Function) {
              
              
        console.log(`Logger: ${
                
                target.name} constructed.`)
      }
      
      @log
      class MyClass {
              
              
        constructor() {
              
              }
      }
      
      // output: Logger: MyClass constructed.
      
    • 在这个例子中,装饰器函数 log 被应用到了 MyClass 类的构造函数上,当代码被编译时,装饰器会改变 MyClass 的行为,使得在构造函数被调用时打印日志。
  • 校验
    • 装饰器可以对类和属性进行校验,以确保它们符合一定的规范。
      // 类装饰器
      function validateClass(target: any) {
              
              
        // 检查类是否有name属性
        if (!target.name) {
              
              
          throw new Error("Class name is required");
        }
      }
      
      // 属性装饰器
      function validateProperty(target: any, propertyKey: string) {
              
              
        // 获取属性值
        const value = target[propertyKey];
        // 检查属性值是否为数字
        if (typeof value !== "number") {
              
              
          throw new Error(`${
                
                propertyKey} must be a number`);
        }
      }
      
      @validateClass
      class Person {
              
              
        name: string;
      
        @validateProperty
        age: number;
      
        constructor(name: string, age: number) {
              
              
          this.name = name;
          this.age = age;
        }
      }
      
      // 创建一个Person实例
      const person = new Person("Bob", 25);
      
      // 输出实例的属性
      console.log(person.name); // "Bob"
      console.log(person.age); // 25
      
      // 创建一个不合法的Person实例
      const invalidPerson = new Person("", "25");
      
      // 抛出类装饰器抛出的异常
      // Error: Class name is required
      
      // 创建一个属性不合法的Person实例
      const invalidAgePerson = new Person("Bob", "25");
      
      // 抛出属性装饰器抛出的异常
      // Error: age must be a number
      
    • 该例子定义了两个装饰器函数 validateClassvalidatePropertyvalidateClass 用于检查类是否有 name 属性,如果没有则抛出异常。 validateProperty 用于检查类的属性值是否为数字,如果不是则抛出异常。
    • Person 类中,用 @validateClass 装饰器装饰类,用 @validateProperty 装饰器装饰 age 属性。在创建 Person 实例时,如果类的属性不符合要求,则会抛出相应的异常。
  • 性能优化
    • 装饰器可以对代码进行性能优化,例如缓存一些计算结果,避免重复计算。
      function memoize(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
              
              
        const originalMethod = descriptor.value;
        const cache = new Map();
      
        descriptor.value = function(...args) {
              
              
          const key = JSON.stringify(args);
          if (cache.has(key)) {
              
              
            return cache.get(key);
          }
          const result = originalMethod.apply(this, args);
          cache.set(key, result);
          return result;
        };
      
        return descriptor;
      }
      
      class Calculator {
              
              
        @memoize
        factorial(n: number): number {
              
              
          console.log(`Calculating factorial of ${
                
                n}`);
          if (n === 0) {
              
              
            return 1;
          }
          return n * this.factorial(n - 1);
        }
      }
      
      const calc = new Calculator();
      
      console.log(calc.factorial(5)); // Calculating factorial of 5, 120
      console.log(calc.factorial(5)); // 120
      
      console.log(calc.factorial(10)); // Calculating factorial of 10, 3628800
      console.log(calc.factorial(10)); // 3628800
      
    • 该例子定义了一个装饰器函数 memoize ,用于缓存类的某一个方法的计算结果。在 memoize 函数中,首先获取原始的方法,然后创建一个缓存 Map 。覆盖原始方法,当调用这个方法时,将传递的参数转换为字符串作为缓存键。如果缓存中已经有了这个键对应的值,就直接返回缓存中的结果,否则就计算方法的结果并将结果存入缓存中。
    • Calculator 类中,使用 @memoize 装饰器装饰 factorial 方法,即将这个方法变成一个具有缓存功能的方法。在创建 Calculator 实例后,调用 factorial 方法两次,第一次需要计算阶乘,而第二次则直接从缓存中获取阶乘值,避免了重复计算。
  • 错误处理
    • 装饰器可以对程序中的错误进行处理,例如捕获异常并输出错误信息,方便开发者检查和排除错误。
      function catchError(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
              
              
        const originalMethod = descriptor.value; // 保存原始方法
        descriptor.value = function (...args: any[]) {
              
              
          try {
              
              
            return originalMethod.apply(this, args); // 调用原始方法并返回结果
          } catch (error) {
              
              
            console.error(`Error occurred in ${
                
                propertyKey}:`, error); // 输出错误信息
          }
        };
      }
      
      class Example {
              
              
        @catchError
        public divide(a: number, b: number): number {
              
              
          return a / b; // 可能会抛出异常
        }
      }
      
      const example = new Example();
      example.divide(10, 0); // 调用方法,抛出异常并输出错误信息
      
    • 在这个例子中,我们定义了一个 catchError 装饰器,并将其应用到了 Example 类的 divide 方法上。当我们调用 example.divide(10, 0) 方法时,由于除数为0会抛出异常,这个装饰器会捕获该异常并输出错误信息。
  • 除了上面的例子,装饰器还可以用于很多其他情况下,比如 控制访问权限验证输入参数缓存数据 等,它为开发者提供了一个更加灵活和强大的元编程工具。

总结

装饰器是一种非常强大的元编程工具,它可以大大增强 TypeScript 的可维护性和扩展性。装饰器可以添加元数据和行为到类、方法、属性和参数上。使用装饰器,开发者们可以更加方便地管理代码,并且更容易地扩展和修改它。

猜你喜欢

转载自blog.csdn.net/McapricornZ/article/details/131295384