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

掌握TypeScript参数装饰器的使用场景与技巧

2024-01-236.3k 阅读

TypeScript 参数装饰器基础概念

在 TypeScript 中,装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、访问器、属性或参数上。参数装饰器是其中针对函数参数进行操作的装饰器类型。

参数装饰器表达式会在运行时当作函数被调用,传入以下三个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象:这个参数提供了关于目标类的基本信息,在处理与类相关的逻辑时非常有用。例如,如果我们想在装饰器中为类添加一些全局配置信息,就可以通过这个参数来实现。
  2. 成员的名字:它表示被装饰的参数所属的方法或属性的名称。通过获取这个名字,我们可以针对不同的方法进行不同的参数处理逻辑。比如,某些特定方法可能需要对参数进行更严格的验证。
  3. 参数在函数参数列表中的索引:这个索引值很关键,因为它能让我们准确地定位到具体的参数。在一个函数可能有多个参数的情况下,我们可以根据这个索引来对特定位置的参数进行操作。

参数装饰器的基本语法

参数装饰器的语法如下:

function parameterDecorator(target: any, propertyKey: string | symbol, parameterIndex: number) {
    // 装饰器逻辑
}

class Example {
    method(@parameterDecorator param: string) {
        // 方法逻辑
    }
}

在上述代码中,parameterDecorator 就是一个参数装饰器。当 Example 类的 method 方法被调用时,parameterDecorator 会被执行,传入 Example.prototype(因为这是实例方法)、'method'(方法名)以及参数在 method 方法参数列表中的索引。

参数装饰器的使用场景

参数验证

在许多应用场景中,对函数参数的验证是至关重要的。使用参数装饰器可以很方便地实现这一功能。例如,我们可能希望某个函数的参数必须是数字类型。

function validateNumber(target: any, propertyKey: string | symbol, parameterIndex: number) {
    return function (target: any, ...args: any[]) {
        if (typeof args[parameterIndex]!== 'number') {
            throw new Error(`The ${parameterIndex + 1}th parameter of ${propertyKey} must be a number.`);
        }
        return Reflect.apply(target, this, args);
    };
}

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

const math = new MathOperations();
// 正确调用
console.log(math.add(2, 3)); 
// 错误调用,会抛出异常
// console.log(math.add('2', 3)); 

在上述代码中,validateNumber 装饰器检查 add 方法的参数是否为数字类型。如果不是,就抛出一个错误。这种方式使得参数验证逻辑与业务逻辑分离,提高了代码的可维护性。

日志记录

参数装饰器还可以用于记录函数参数的调用信息,这对于调试和性能分析非常有帮助。

function logParameter(target: any, propertyKey: string | symbol, parameterIndex: number) {
    return function (target: any, ...args: any[]) {
        console.log(`Calling ${propertyKey}, parameter at index ${parameterIndex} is:`, args[parameterIndex]);
        return Reflect.apply(target, this, args);
    };
}

class UserService {
    getUserById(@logParameter id: number) {
        // 模拟获取用户逻辑
        return { id, name: 'John Doe' };
    }
}

const userService = new UserService();
userService.getUserById(1);

上述代码中,logParameter 装饰器在 getUserById 方法调用时,记录了被装饰参数的值。这使得开发人员在调试过程中能够快速了解方法调用时的参数情况。

参数转换

有时候,我们可能需要对传入的参数进行转换。例如,将字符串类型的数字转换为真正的数字类型。

function convertToNumber(target: any, propertyKey: string | symbol, parameterIndex: number) {
    return function (target: any, ...args: any[]) {
        if (typeof args[parameterIndex] ==='string') {
            args[parameterIndex] = parseInt(args[parameterIndex]);
        }
        return Reflect.apply(target, this, args);
    };
}

class Calculation {
    multiply(@convertToNumber a: number, b: number) {
        return a * b;
    }
}

const calculation = new Calculation();
// 传入字符串类型的数字,会被转换为数字
console.log(calculation.multiply('2', 3)); 

在这个例子中,convertToNumber 装饰器检查参数是否为字符串类型,如果是,则将其转换为数字类型,然后再执行 multiply 方法。

参数装饰器与依赖注入

依赖注入是一种软件设计模式,它允许将依赖对象传递给一个类,而不是在类内部创建它们。参数装饰器可以很好地与依赖注入机制相结合。

简单的依赖注入示例

假设我们有一个服务类 LoggerService,它用于记录日志。我们希望在另一个类 UserController 的方法中注入这个 LoggerService

class LoggerService {
    log(message: string) {
        console.log(`[LOG] ${message}`);
    }
}

function injectLogger(target: any, propertyKey: string | symbol, parameterIndex: number) {
    return function (target: any, ...args: any[]) {
        const logger = new LoggerService();
        args[parameterIndex] = logger;
        return Reflect.apply(target, this, args);
    };
}

class UserController {
    createUser(@injectLogger logger: LoggerService, user: { name: string }) {
        logger.log(`Creating user ${user.name}`);
        // 实际创建用户逻辑
    }
}

const userController = new UserController();
userController.createUser({ name: 'Alice' });

在上述代码中,injectLogger 装饰器为 createUser 方法注入了 LoggerService 实例。这样,createUser 方法就可以使用 LoggerService 来记录日志,而无需在方法内部手动创建 LoggerService 实例。

基于容器的依赖注入

在更复杂的应用中,通常会使用依赖注入容器来管理依赖关系。我们可以结合参数装饰器和依赖注入容器来实现更灵活的依赖注入。

class LoggerService {
    log(message: string) {
        console.log(`[LOG] ${message}`);
    }
}

class DatabaseService {
    connect() {
        console.log('Connecting to database...');
    }
}

class Container {
    private dependencies: { [key: string]: any } = {};

    register(key: string, dependency: any) {
        this.dependencies[key] = dependency;
    }

    resolve(key: string) {
        return this.dependencies[key];
    }
}

const container = new Container();
container.register('LoggerService', new LoggerService());
container.register('DatabaseService', new DatabaseService());

function inject(key: string) {
    return function (target: any, propertyKey: string | symbol, parameterIndex: number) {
        return function (target: any, ...args: any[]) {
            const dependency = container.resolve(key);
            args[parameterIndex] = dependency;
            return Reflect.apply(target, this, args);
        };
    };
}

class UserRepository {
    constructor(@inject('DatabaseService') private databaseService: DatabaseService) {}

    saveUser(user: { name: string }) {
        this.databaseService.connect();
        // 实际保存用户逻辑
    }
}

class UserController {
    constructor(@inject('LoggerService') private logger: LoggerService) {}

    createUser(user: { name: string }) {
        this.logger.log(`Creating user ${user.name}`);
        const userRepository = new UserRepository();
        userRepository.saveUser(user);
    }
}

const userController = new UserController();
userController.createUser({ name: 'Bob' });

在这个示例中,我们定义了一个简单的依赖注入容器 Containerinject 装饰器通过这个容器来解析并注入依赖。UserRepository 类通过参数装饰器注入了 DatabaseServiceUserController 类注入了 LoggerService。这种方式使得依赖关系更加清晰,易于管理和维护。

参数装饰器的高级技巧

多个参数装饰器的组合使用

在实际开发中,一个函数参数可能需要多个装饰器来实现不同的功能。例如,我们可能既需要对参数进行验证,又需要记录参数日志。

function validateNumber(target: any, propertyKey: string | symbol, parameterIndex: number) {
    return function (target: any, ...args: any[]) {
        if (typeof args[parameterIndex]!== 'number') {
            throw new Error(`The ${parameterIndex + 1}th parameter of ${propertyKey} must be a number.`);
        }
        return Reflect.apply(target, this, args);
    };
}

function logParameter(target: any, propertyKey: string | symbol, parameterIndex: number) {
    return function (target: any, ...args: any[]) {
        console.log(`Calling ${propertyKey}, parameter at index ${parameterIndex} is:`, args[parameterIndex]);
        return Reflect.apply(target, this, args);
    };
}

class MathOperations {
    divide(@validateNumber @logParameter a: number, @validateNumber @logParameter b: number) {
        if (b === 0) {
            throw new Error('Division by zero');
        }
        return a / b;
    }
}

const math = new MathOperations();
// 正确调用
console.log(math.divide(10, 2)); 
// 错误调用,会先触发验证错误
// console.log(math.divide(10, '2')); 

在上述代码中,divide 方法的参数同时应用了 validateNumberlogParameter 装饰器。这两个装饰器按照顺序依次执行,先验证参数类型,然后记录参数日志。

装饰器工厂函数

有时候,我们需要根据不同的条件创建不同的装饰器。这时候就可以使用装饰器工厂函数。

function validateType(type: string) {
    return function (target: any, propertyKey: string | symbol, parameterIndex: number) {
        return function (target: any, ...args: any[]) {
            if (typeof args[parameterIndex]!== type) {
                throw new Error(`The ${parameterIndex + 1}th parameter of ${propertyKey} must be of type ${type}.`);
            }
            return Reflect.apply(target, this, args);
        };
    };
}

class Example {
    processData(@validateType('string') data: string) {
        // 处理数据逻辑
        console.log('Processing data:', data);
    }
}

const example = new Example();
// 正确调用
example.processData('Hello'); 
// 错误调用,会抛出异常
// example.processData(123); 

在这个例子中,validateType 是一个装饰器工厂函数,它接受一个类型字符串作为参数,并返回一个参数装饰器。通过这种方式,我们可以根据需要动态创建不同类型验证的装饰器。

装饰器与元数据

TypeScript 提供了 Reflect 元数据 API,我们可以结合参数装饰器来使用它。元数据可以用于存储与类、方法、属性或参数相关的额外信息。

import 'reflect-metadata';

const PARAM_METADATA = 'design:paramtypes';

function markRequired(target: any, propertyKey: string | symbol, parameterIndex: number) {
    const existingRequired = Reflect.getOwnMetadata(PARAM_METADATA, target, propertyKey) || [];
    existingRequired[parameterIndex] = true;
    Reflect.defineMetadata(PARAM_METADATA, existingRequired, target, propertyKey);
}

function validateRequired(target: any, propertyKey: string | symbol) {
    return function (target: any, ...args: any[]) {
        const requiredParams = Reflect.getOwnMetadata(PARAM_METADATA, target, propertyKey) || [];
        requiredParams.forEach((isRequired, index) => {
            if (isRequired && args[index] === undefined) {
                throw new Error(`The ${index + 1}th parameter of ${propertyKey} is required.`);
            }
        });
        return Reflect.apply(target, this, args);
    };
}

class User {
    constructor(@markRequired public name: string, @markRequired public age: number) {}

    @validateRequired
    printInfo() {
        console.log(`Name: ${this.name}, Age: ${this.age}`);
    }
}

// 正确调用
const user = new User('Charlie', 30);
user.printInfo(); 
// 错误调用,会抛出异常
// const user2 = new User('Eve'); 

在上述代码中,markRequired 装饰器使用 Reflect 元数据 API 来标记某个参数为必需的。validateRequired 装饰器则在方法调用时检查这些标记,确保所有必需的参数都已提供。

参数装饰器在不同框架中的应用

在 Angular 中的应用

Angular 是一个流行的前端框架,它广泛使用装饰器来实现依赖注入、组件定义等功能。虽然 Angular 有自己的依赖注入机制,但我们也可以通过自定义参数装饰器来扩展功能。

import { Injectable } from '@angular/core';

@Injectable()
class LoggerService {
    log(message: string) {
        console.log(`[ANGULAR LOG] ${message}`);
    }
}

function injectLogger(target: any, propertyKey: string | symbol, parameterIndex: number) {
    return function (target: any, ...args: any[]) {
        const logger = new LoggerService();
        args[parameterIndex] = logger;
        return Reflect.apply(target, this, args);
    };
}

class UserComponent {
    constructor(@injectLogger private logger: LoggerService) {}

    createUser(user: { name: string }) {
        this.logger.log(`Creating user ${user.name}`);
        // 实际创建用户逻辑
    }
}

在这个简单的 Angular 示例中,我们通过自定义参数装饰器 injectLoggerUserComponent 的构造函数注入了 LoggerService。这展示了如何在 Angular 中利用参数装饰器来实现自定义的依赖注入逻辑。

在 Vue 中的应用

Vue 是另一个常用的前端框架,虽然 Vue 本身对装饰器的支持相对有限,但通过一些插件(如 vue - class - component)可以在 Vue 组件中使用装饰器。

import Vue from 'vue';
import Component from 'vue-class-component';

class LoggerService {
    log(message: string) {
        console.log(`[VUE LOG] ${message}`);
    }
}

function injectLogger(target: any, propertyKey: string | symbol, parameterIndex: number) {
    return function (target: any, ...args: any[]) {
        const logger = new LoggerService();
        args[parameterIndex] = logger;
        return Reflect.apply(target, this, args);
    };
}

@Component
export default class UserComponent extends Vue {
    @injectLogger logger: LoggerService;

    created() {
        this.logger.log('Component created');
    }
}

在这个 Vue 示例中,借助 vue - class - component 插件,我们在 UserComponent 中使用了自定义参数装饰器 injectLogger 来注入 LoggerService。这为在 Vue 中使用参数装饰器提供了一种可行的方式。

注意事项

  1. 装饰器的执行顺序:当一个参数有多个装饰器时,它们会从最靠近参数的装饰器开始,由内向外依次执行。例如,@decorator1 @decorator2 param,先执行 decorator2,再执行 decorator1

  2. 装饰器与类的继承:在类继承的情况下,参数装饰器只会应用在子类中被重写的方法上。如果子类没有重写父类的方法,父类方法上的参数装饰器不会在子类实例调用该方法时执行。

  3. 装饰器的兼容性:虽然 TypeScript 支持装饰器,但不同的运行环境对装饰器的支持可能有所不同。在实际应用中,需要确保目标运行环境能够正确运行使用了装饰器的代码。例如,在一些旧版本的 JavaScript 运行时中,可能需要使用转译工具(如 Babel)来将装饰器代码转换为兼容的形式。

  4. 性能影响:虽然参数装饰器在大多数情况下不会对性能产生明显影响,但如果装饰器逻辑过于复杂,特别是在频繁调用的函数参数上使用复杂装饰器逻辑,可能会对性能产生一定的影响。因此,在编写装饰器时,应尽量保持逻辑的简洁和高效。

通过深入理解和灵活运用 TypeScript 参数装饰器的各种使用场景与技巧,开发人员能够更好地组织和管理代码,提高代码的可维护性、可测试性和复用性,从而打造出更加健壮和高效的前端应用。无论是参数验证、日志记录、依赖注入,还是与各种前端框架的结合应用,参数装饰器都为前端开发带来了更多的灵活性和便利性。同时,注意装饰器使用过程中的各种细节和注意事项,能够避免潜在的问题,确保代码的稳定运行。