引言
在 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!"
-
在这个例子中,我们使用了
Reflect
的getMetadata
方法来获取元数据。这个方法的第一个参数是元数据的 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
- 该例子定义了两个装饰器函数
validateClass
和validateProperty
。validateClass
用于检查类是否有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 的可维护性和扩展性。装饰器可以添加元数据和行为到类、方法、属性和参数上。使用装饰器,开发者们可以更加方便地管理代码,并且更容易地扩展和修改它。