MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript装饰器入门:基础概念与简单示例

2021-01-072.0k 阅读

什么是 TypeScript 装饰器

TypeScript 装饰器是一种特殊类型的声明,它能够附加到类声明、方法、属性或参数上,为它们添加额外的行为或元数据。装饰器本质上是一个函数,这个函数会在被装饰的目标(类、方法等)被定义时执行,并且可以对目标进行修改。

装饰器在很多场景下都非常有用,例如日志记录、权限验证、性能测量等。通过使用装饰器,可以将这些横切关注点从业务逻辑中分离出来,提高代码的可维护性和可复用性。

装饰器的类型

  1. 类装饰器:应用于类的声明,用于修改类的定义。
  2. 方法装饰器:应用于类的方法,可用于修改方法的属性描述符或添加额外的行为。
  3. 属性装饰器:应用于类的属性,可用于添加元数据或修改属性的特性。
  4. 参数装饰器:应用于类方法的参数,可用于收集关于参数的信息。

基础语法

装饰器的语法使用 @ 符号加上装饰器函数名,紧跟在被装饰的目标之前。例如:

@logClass
class MyClass {
    @logMethod
    myMethod(@logParameter param: string) {
        console.log(`Method called with parameter: ${param}`);
    }
}

在上述代码中,@logClass 是类装饰器,@logMethod 是方法装饰器,@logParameter 是参数装饰器。

类装饰器

类装饰器是应用于类声明的装饰器。它接收一个参数,即被装饰类的构造函数。通过这个构造函数,我们可以修改类的行为或添加新的属性和方法。

示例:简单的类装饰器

function logClass(target: Function) {
    console.log(`Class ${target.name} has been decorated`);
}

@logClass
class MyClass {
    message: string;
    constructor(message: string) {
        this.message = message;
    }
    printMessage() {
        console.log(this.message);
    }
}

在上述示例中,logClass 是一个简单的类装饰器。当 MyClass 被定义时,logClass 函数会被执行,输出 Class MyClass has been decorated

示例:增强类的功能

function enhanceClass(target: Function) {
    return class extends target {
        newMethod() {
            console.log('This is a new method added by the decorator');
        }
    };
}

@enhanceClass
class MyBaseClass {
    baseMethod() {
        console.log('This is a base method');
    }
}

let instance = new MyBaseClass();
instance.baseMethod();
// 这里会报错,因为在原类中没有 newMethod
// instance.newMethod(); 
// 如果想使用 newMethod,需要这样做
let enhancedInstance = new (enhanceClass(MyBaseClass))();
enhancedInstance.newMethod();

在这个示例中,enhanceClass 装饰器返回一个新的类,这个类继承自被装饰的类,并添加了一个新的方法 newMethod

方法装饰器

方法装饰器应用于类的方法。它接收三个参数:

  1. 目标对象:对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象。
  2. 方法名:被装饰方法的名称。
  3. 属性描述符:方法的属性描述符,通过它可以修改方法的行为,如 valuewritableenumerableconfigurable 等。

示例:记录方法调用日志

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

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

let mathOps = new MathOperations();
mathOps.add(2, 3);

在上述示例中,logMethod 装饰器记录了方法调用的参数和返回值。它首先保存原始方法,然后重新定义 descriptor.value,在调用原始方法前后添加日志记录。

示例:改变方法的可枚举性

function makeMethodNonEnumerable(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = false;
    return descriptor;
}

class MyClassWithMethod {
    @makeMethodNonEnumerable
    myMethod() {
        console.log('This is my method');
    }
}

let myObj = new MyClassWithMethod();
for (let prop in myObj) {
    if (myObj.hasOwnProperty(prop)) {
        console.log(prop);
    }
}
// 这里不会输出 myMethod,因为它已经不可枚举

在这个示例中,makeMethodNonEnumerable 装饰器将方法的 enumerable 属性设置为 false,使得该方法在使用 for...in 循环遍历时不会被列出。

属性装饰器

属性装饰器应用于类的属性。它接收两个参数:

  1. 目标对象:对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象。
  2. 属性名:被装饰属性的名称。

属性装饰器通常用于添加元数据,而不是直接修改属性的行为。

示例:添加属性元数据

function addMetadata(target: any, propertyKey: string) {
    Reflect.defineMetadata('description', `This is the ${propertyKey} property`, target, propertyKey);
}

class MyMetadataClass {
    @addMetadata
    myProperty: string;
    constructor(value: string) {
        this.myProperty = value;
    }
}

let metadataObj = new MyMetadataClass('Some value');
let description = Reflect.getMetadata('description', metadataObj,'myProperty');
console.log(description);

在上述示例中,addMetadata 装饰器使用 Reflect.defineMetadata 方法为 myProperty 属性添加了一个描述元数据。然后通过 Reflect.getMetadata 方法获取该元数据并输出。

参数装饰器

参数装饰器应用于类方法的参数。它接收三个参数:

  1. 目标对象:对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象。
  2. 方法名:被装饰方法的名称。
  3. 参数索引:参数在方法参数列表中的位置索引。

参数装饰器通常用于收集关于参数的信息,例如验证参数类型或记录参数的使用情况。

示例:记录参数使用

function logParameter(target: any, propertyKey: string, parameterIndex: number) {
    let existingLog = Reflect.getMetadata('parameterUsage', target, propertyKey) || [];
    existingLog.push(`Parameter at index ${parameterIndex} has been used`);
    Reflect.defineMetadata('parameterUsage', existingLog, target, propertyKey);
}

class ParameterLoggerClass {
    logMessage(@logParameter message: string) {
        console.log(`Message: ${message}`);
    }
}

let loggerObj = new ParameterLoggerClass();
loggerObj.logMessage('Hello, world!');
let usageLog = Reflect.getMetadata('parameterUsage', loggerObj, 'logMessage');
console.log(usageLog);

在这个示例中,logParameter 装饰器记录了方法参数的使用情况。它使用 Reflect.getMetadataReflect.defineMetadata 方法来管理和存储参数使用的日志信息。

装饰器工厂

装饰器工厂是一个返回装饰器函数的函数。它允许我们在定义装饰器时传入参数,从而创建不同配置的装饰器。

示例:带有参数的日志装饰器

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

class PrefixLoggerClass {
    @logWithPrefix('DEBUG: ')
    debugMethod(a: number, b: number) {
        return a + b;
    }

    @logWithPrefix('INFO: ')
    infoMethod(a: number, b: number) {
        return a * b;
    }
}

let prefixLogger = new PrefixLoggerClass();
prefixLogger.debugMethod(2, 3);
prefixLogger.infoMethod(4, 5);

在上述示例中,logWithPrefix 是一个装饰器工厂。它接收一个 prefix 参数,并返回一个装饰器函数。通过这个工厂,我们可以创建不同前缀的日志装饰器,如 DEBUG: INFO:

装饰器的执行顺序

  1. 参数装饰器:按照参数从左到右的顺序执行。
  2. 方法装饰器:在参数装饰器之后执行。
  3. 属性装饰器:在方法装饰器之后执行。
  4. 类装饰器:最后执行。

示例:验证执行顺序

function paramDecorator(target: any, propertyKey: string, parameterIndex: number) {
    console.log(`Parameter decorator at index ${parameterIndex}`);
}

function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(`Method decorator for ${propertyKey}`);
}

function propertyDecorator(target: any, propertyKey: string) {
    console.log(`Property decorator for ${propertyKey}`);
}

function classDecorator(target: Function) {
    console.log(`Class decorator for ${target.name}`);
}

class ExecutionOrderClass {
    @propertyDecorator
    myProperty: string;

    @methodDecorator
    myMethod(@paramDecorator param: string) {
        console.log(`Method called with parameter: ${param}`);
    }
}

@classDecorator
class OuterExecutionOrderClass extends ExecutionOrderClass { }

在上述示例中,运行代码时会按照参数装饰器、方法装饰器、属性装饰器、类装饰器的顺序输出日志信息,验证了装饰器的执行顺序。

装饰器与 ES 标准

目前,装饰器还处于 ECMAScript 标准的提案阶段,尚未完全标准化。TypeScript 对装饰器的支持是基于实验性的实现。在使用装饰器时,需要注意以下几点:

  1. 编译选项:在 TypeScript 编译时,需要启用 experimentalDecorators 选项,例如在 tsconfig.json 中添加 "experimentalDecorators": true
  2. 兼容性:由于装饰器不是标准的 JavaScript 特性,在运行时可能需要使用一些转译工具(如 Babel)来确保在不同环境中正常工作。

总结

TypeScript 装饰器为我们提供了一种强大的方式来为类、方法、属性和参数添加额外的行为和元数据。通过合理使用装饰器,可以使代码更加模块化、可维护和可复用。然而,由于装饰器的实验性,在实际项目中使用时需要谨慎,并充分考虑兼容性和未来标准的变化。在日常开发中,可以根据具体需求,灵活运用类装饰器、方法装饰器、属性装饰器和参数装饰器,以及装饰器工厂,来优化代码结构和提高开发效率。希望通过本文的介绍和示例,你对 TypeScript 装饰器有了更深入的理解,并能够在自己的项目中合理地运用它们。