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

深入理解TypeScript方法装饰器的实现与应用

2023-07-104.4k 阅读

方法装饰器基础概念

在TypeScript中,装饰器是一种特殊的语法,用于在类的声明及成员上进行标注和元编程。方法装饰器是装饰器的一种类型,它作用于类的方法上。

方法装饰器的声明形式如下:

function methodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor): PropertyDescriptor | void {
    // 装饰器逻辑
    return descriptor;
}
  • target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • propertyKey:方法的名字。
  • descriptor:包含了方法的属性描述符,例如 value(方法的实际实现)、writable(是否可写)、enumerable(是否可枚举)和 configurable(是否可配置)。

简单示例 - 日志记录

假设我们有一个简单的 Calculator 类,其中有一个 add 方法,我们希望在每次调用 add 方法时记录日志。

function logMethod(target: Object, propertyKey: string | symbol, 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 Calculator {
    @logMethod
    add(a: number, b: number): number {
        return a + b;
    }
}

const calculator = new Calculator();
const sum = calculator.add(2, 3);

在上述代码中,logMethod 装饰器接收 targetpropertyKeydescriptor。它首先保存原始方法 originalMethod,然后重新定义 descriptor.value。新的方法在调用原始方法前后打印日志,这样每次调用 add 方法时都会记录输入参数和返回值。

装饰器的执行时机

方法装饰器在类定义时就会执行。这意味着,一旦类被定义,装饰器逻辑就会立即生效,而不是在方法被调用时。

function executionLogger(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    console.log(`Decorator for method ${propertyKey} is being executed`);
    return descriptor;
}

class ExampleClass {
    @executionLogger
    exampleMethod() {
        console.log('Inside exampleMethod');
    }
}
// 输出: Decorator for method exampleMethod is being executed
const example = new ExampleClass();
// 调用方法时不会再次触发装饰器的打印
example.exampleMethod();

在这个例子中,当 ExampleClass 被定义时,executionLogger 装饰器中的 console.log 就会执行,而在 exampleMethod 被调用时,不会再次执行装饰器中的 console.log 语句。

方法装饰器与函数重载

在TypeScript中,函数重载允许一个函数根据不同的参数列表有不同的行为。方法装饰器同样可以与函数重载一起使用。

function overloadLogger(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Calling overloaded method ${propertyKey} with arguments:`, args);
        return originalMethod.apply(this, args);
    };
    return descriptor;
}

class OverloadExample {
    @overloadLogger
    greet(): string;
    @overloadLogger
    greet(name: string): string;
    @overloadLogger
    greet(name?: string): string {
        if (name) {
            return `Hello, ${name}!`;
        }
        return 'Hello!';
    }
}

const overloadExample = new OverloadExample();
console.log(overloadExample.greet());
console.log(overloadExample.greet('John'));

在这个 OverloadExample 类中,greet 方法有两个重载定义。overloadLogger 装饰器能够应用于所有的重载定义以及实际的实现。每次调用 greet 方法时,都会打印调用信息,无论使用的是哪个重载形式。

装饰器工厂函数

装饰器工厂函数是一个返回装饰器的函数。这种模式在需要向装饰器传递参数时非常有用。

function delayDecoratorFactory(delay: number) {
    return function delayDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            return new Promise((resolve) => {
                setTimeout(() => {
                    const result = originalMethod.apply(this, args);
                    if (result && typeof result.then === 'function') {
                        result.then(resolve);
                    } else {
                        resolve(result);
                    }
                }, delay);
            });
        };
        return descriptor;
    };
}

class DelayedExecutor {
    @delayDecoratorFactory(2000)
    execute() {
        console.log('Executing...');
        return 'Execution result';
    }
}

const delayedExecutor = new DelayedExecutor();
delayedExecutor.execute().then((result) => {
    console.log('Result after delay:', result);
});

在上述代码中,delayDecoratorFactory 是一个装饰器工厂函数,它接收一个 delay 参数。delayDecoratorFactory 返回 delayDecorator 装饰器。DelayedExecutor 类的 execute 方法使用 delayDecoratorFactory(2000) 进行装饰,这意味着每次调用 execute 方法时,会延迟2秒执行,并返回一个 Promise

多个装饰器的应用

一个方法可以应用多个装饰器。装饰器的执行顺序是从下到上(或者说从内到外)。

function firstDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    console.log('First decorator is called');
    return descriptor;
}

function secondDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    console.log('Second decorator is called');
    return descriptor;
}

class MultipleDecoratorsExample {
    @firstDecorator
    @secondDecorator
    exampleMethod() {
        console.log('Inside exampleMethod');
    }
}
// 输出: Second decorator is called
// 输出: First decorator is called
const multipleDecoratorsExample = new MultipleDecoratorsExample();
multipleDecoratorsExample.exampleMethod();

MultipleDecoratorsExample 类中,exampleMethod 应用了 firstDecoratorsecondDecorator。当类被定义时,secondDecorator 先被调用,然后是 firstDecorator

方法装饰器与类继承

当一个类继承自另一个使用了方法装饰器的类时,装饰器会被继承。

function inheritedDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log('Before calling inherited method');
        const result = originalMethod.apply(this, args);
        console.log('After calling inherited method');
        return result;
    };
    return descriptor;
}

class BaseClass {
    @inheritedDecorator
    baseMethod() {
        console.log('Inside baseMethod');
    }
}

class SubClass extends BaseClass {
    // 虽然没有再次标注,但继承了装饰器
    baseMethod() {
        super.baseMethod();
        console.log('Inside subClass baseMethod after super call');
    }
}

const subClass = new SubClass();
subClass.baseMethod();

在这个例子中,BaseClassbaseMethod 使用了 inheritedDecoratorSubClass 继承自 BaseClass 并覆盖了 baseMethod。尽管 SubClass 没有再次标注 inheritedDecorator,但由于继承,subClass.baseMethod 调用时仍然会执行装饰器逻辑。

方法装饰器与类型兼容性

从类型兼容性的角度来看,方法装饰器不会改变方法的类型签名。也就是说,装饰后的方法类型与原始方法类型保持一致。

function typeSafeDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    return descriptor;
}

class TypeSafeExample {
    @typeSafeDecorator
    typeSafeMethod(a: number, b: number): number {
        return a + b;
    }
}

const typeSafeExample = new TypeSafeExample();
let result: number;
// 类型检查通过,因为装饰器没有改变方法类型
result = typeSafeExample.typeSafeMethod(2, 3);

在上述代码中,typeSafeMethodtypeSafeDecorator 装饰后,其类型签名 (a: number, b: number) => number 保持不变,因此类型检查能够正常通过。

方法装饰器与元数据

TypeScript 提供了 reflect - metadata 库来处理元数据。方法装饰器可以利用元数据来存储和检索与方法相关的额外信息。

import 'reflect - metadata';

const metadataKey = 'custom:metadata';

function metadataDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    Reflect.defineMetadata(metadataKey, 'Some custom metadata', target, propertyKey);
    return descriptor;
}

class MetadataExample {
    @metadataDecorator
    metadataMethod() {
        console.log('Inside metadataMethod');
    }
}

const metadataExample = new MetadataExample();
const metadata = Reflect.getMetadata(metadataKey, metadataExample,'metadataMethod');
console.log('Retrieved metadata:', metadata);

在这个例子中,metadataDecorator 使用 Reflect.defineMetadata 方法在 metadataMethod 上定义了一个自定义元数据。之后,通过 Reflect.getMetadata 方法可以检索到这个元数据。

方法装饰器在实际项目中的应用场景

  1. 权限控制:在企业级应用中,某些方法可能只允许特定角色的用户访问。可以使用方法装饰器来检查当前用户的角色,并决定是否允许执行该方法。
function requireRole(role: string) {
    return function roleDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            const currentUserRole = getCurrentUserRole();// 假设这个函数获取当前用户角色
            if (currentUserRole === role) {
                return originalMethod.apply(this, args);
            } else {
                throw new Error('Access denied');
            }
        };
        return descriptor;
    };
}

function getCurrentUserRole() {
    // 实际实现获取当前用户角色
    return 'admin';
}

class AdminPanel {
    @requireRole('admin')
    deleteUser(userId: string) {
        console.log(`Deleting user with ID ${userId}`);
    }
}

const adminPanel = new AdminPanel();
adminPanel.deleteUser('123');

在这个 AdminPanel 类中,deleteUser 方法使用 requireRole('admin') 装饰,只有当当前用户角色为 admin 时才能调用该方法。

  1. 性能监控:在性能敏感的应用中,方法装饰器可用于测量方法执行时间,以便找出性能瓶颈。
function performanceMonitor(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        const start = Date.now();
        const result = originalMethod.apply(this, args);
        const end = Date.now();
        console.log(`Method ${propertyKey} took ${end - start} ms to execute`);
        return result;
    };
    return descriptor;
}

class PerformanceExample {
    @performanceMonitor
    complexCalculation() {
        // 模拟复杂计算
        let sum = 0;
        for (let i = 0; i < 1000000; i++) {
            sum += i;
        }
        return sum;
    }
}

const performanceExample = new PerformanceExample();
performanceExample.complexCalculation();

PerformanceExample 类中,complexCalculation 方法被 performanceMonitor 装饰,每次调用该方法时,都会打印出方法执行所花费的时间。

  1. 数据验证:在处理用户输入或外部数据时,确保数据的有效性至关重要。方法装饰器可以用于验证方法参数的合法性。
function validateNumber(target: Object, propertyKey: string | symbol, parameterIndex: number) {
    return function parameterDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            if (typeof args[parameterIndex]!== 'number') {
                throw new Error(`Parameter at index ${parameterIndex} must be a number`);
            }
            return originalMethod.apply(this, args);
        };
        return descriptor;
    };
}

class DataValidationExample {
    calculateSum(@validateNumber sum1: number, @validateNumber sum2: number) {
        return sum1 + sum2;
    }
}

const dataValidationExample = new DataValidationExample();
try {
    dataValidationExample.calculateSum(2, 3);
    dataValidationExample.calculateSum(2, 'three');// 会抛出错误
} catch (error) {
    console.error(error);
}

DataValidationExample 类中,calculateSum 方法的参数使用 validateNumber 装饰,确保传入的参数是数字类型,否则抛出错误。

方法装饰器的局限性

  1. 不支持原生JavaScript运行时:方法装饰器是TypeScript扩展的语法,在原生JavaScript运行时并不直接支持。这意味着如果项目需要在不支持装饰器的环境中运行,可能需要进行额外的编译步骤,如使用Babel进行转译。
  2. 复杂场景下的调试困难:当多个装饰器组合使用,或者装饰器逻辑较为复杂时,调试会变得困难。由于装饰器在类定义时执行,调试工具可能难以跟踪装饰器的执行流程,尤其是在大型项目中。
  3. 潜在的性能影响:虽然现代JavaScript引擎已经做了很多优化,但装饰器的使用,特别是在方法调用路径上添加额外逻辑,可能会对性能产生一定影响。例如,每次方法调用都执行日志记录或权限检查等装饰器逻辑,可能会增加方法的执行时间。

与其他前端框架结合使用

  1. 在React中使用:在React项目中,可以将方法装饰器用于组件类的方法。例如,在一个需要进行权限控制的React组件中:
import React, { Component } from'react';

function requireRole(role: string) {
    return function roleDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            const currentUserRole = getCurrentUserRole();
            if (currentUserRole === role) {
                return originalMethod.apply(this, args);
            } else {
                throw new Error('Access denied');
            }
        };
        return descriptor;
    };
}

function getCurrentUserRole() {
    return 'admin';
}

class AdminComponent extends Component {
    @requireRole('admin')
    handleDelete() {
        console.log('Deleting item...');
    }

    render() {
        return (
            <div>
                <button onClick={this.handleDelete.bind(this)}>Delete</button>
            </div>
        );
    }
}

在这个React组件 AdminComponent 中,handleDelete 方法使用 requireRole('admin') 装饰,只有管理员角色的用户点击按钮时才能执行删除操作。

  1. 在Vue中使用:在Vue项目中,如果使用TypeScript,也可以在Vue组件的方法上应用装饰器。例如,对于一个需要性能监控的Vue组件:
import Vue from 'vue';

function performanceMonitor(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        const start = Date.now();
        const result = originalMethod.apply(this, args);
        const end = Date.now();
        console.log(`Method ${propertyKey} took ${end - start} ms to execute`);
        return result;
    };
    return descriptor;
}

export default Vue.extend({
    methods: {
        @performanceMonitor
        complexCalculation() {
            let sum = 0;
            for (let i = 0; i < 1000000; i++) {
                sum += i;
            }
            return sum;
        }
    }
});

在这个Vue组件中,complexCalculation 方法被 performanceMonitor 装饰,每次调用该方法时会打印执行时间。

通过以上对TypeScript方法装饰器的深入探讨,从基础概念到实际应用,以及与前端框架的结合,我们可以看到方法装饰器在前端开发中是一个强大且灵活的工具,能为代码的可维护性、可扩展性和功能性带来显著提升。同时,我们也需要注意其局限性,合理地在项目中使用。