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

TypeScript装饰器的原理与应用场景

2024-01-223.1k 阅读

一、TypeScript装饰器基础概念

在TypeScript中,装饰器是一种特殊的声明,可以附加到类声明、方法、属性或参数上,用于为这些目标添加额外的行为或元数据。它是一种元编程的概念,允许开发者在不改变现有代码逻辑的基础上,对代码进行功能增强。

1.1 装饰器的语法

装饰器本质上是一个函数,它以目标(类、方法、属性等)作为参数,并可以返回一个新的目标或对目标进行修改。装饰器函数的定义形式如下:

function myDecorator(target: any) {
    // 在这里对目标进行操作
    console.log('这是一个装饰器,目标是:', target);
    return target;
}

使用装饰器时,在目标之前加上@符号,后面跟着装饰器函数名:

@myDecorator
class MyClass {
    // 类的定义
}

上述代码中,@myDecorator装饰器应用到了MyClass类上,当MyClass类被定义时,myDecorator函数会被调用,参数target就是MyClass类的构造函数。

1.2 装饰器的类型

TypeScript中有几种不同类型的装饰器,分别应用于不同的目标:

  • 类装饰器:应用于类的定义,用于修改类的行为或添加元数据。
  • 方法装饰器:应用于类的方法,用于修改方法的行为、参数或返回值。
  • 属性装饰器:应用于类的属性,用于修改属性的行为或添加元数据。
  • 参数装饰器:应用于方法的参数,用于获取参数的元数据或修改参数行为。

二、类装饰器

2.1 类装饰器的原理

类装饰器在类声明之前声明,它接收类的构造函数作为参数。通过操作这个构造函数,我们可以修改类的行为。例如,我们可以为类添加新的属性或方法,或者修改类的原型。

function classDecorator(target: Function) {
    target.prototype.newMethod = function() {
        console.log('这是通过类装饰器添加的新方法');
    };
    return target;
}

@classDecorator
class MyClass {
    // 类的定义
}

const myObject = new MyClass();
myObject.newMethod(); // 输出:这是通过类装饰器添加的新方法

在上述代码中,classDecorator装饰器接收MyClass类的构造函数target,通过在target.prototype上添加newMethod方法,为MyClass类的所有实例添加了一个新方法。

2.2 类装饰器的应用场景

  • 日志记录:可以在类实例化时记录日志。
function logClass(target: Function) {
    return class extends target {
        constructor(...args: any[]) {
            console.log(`正在实例化 ${target.name}`);
            super(...args);
        }
    };
}

@logClass
class User {
    constructor(public name: string) {}
}

const user = new User('John'); // 输出:正在实例化 User
  • 单例模式实现:通过装饰器可以方便地将一个类转换为单例模式。
function singleton(target: Function) {
    let instance: any;
    return class extends target {
        constructor(...args: any[]) {
            if (!instance) {
                instance = super(...args);
            }
            return instance;
        }
    };
}

@singleton
class Database {
    private constructor() {}
    public connect() {
        console.log('连接到数据库');
    }
}

const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // 输出:true

三、方法装饰器

3.1 方法装饰器的原理

方法装饰器应用于类的方法声明之前,它接收三个参数:

  • target:对于静态方法,它是类的构造函数;对于实例方法,它是类的原型对象。
  • propertyKey:方法的名称。
  • descriptor:方法的属性描述符,通过它可以修改方法的行为,如是否可枚举、是否可写等。
function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
        console.log(`调用方法 ${propertyKey} 之前`);
        const result = originalMethod.apply(this, args);
        console.log(`调用方法 ${propertyKey} 之后`);
        return result;
    };
    return descriptor;
}

class MathUtils {
    @methodDecorator
    add(a: number, b: number) {
        return a + b;
    }
}

const math = new MathUtils();
math.add(2, 3);
// 输出:
// 调用方法 add 之前
// 调用方法 add 之后

在上述代码中,methodDecorator装饰器修改了add方法的行为,在方法调用前后打印日志。

3.2 方法装饰器的应用场景

  • 权限验证:在方法调用前检查用户是否有权限执行该方法。
interface User {
    role: string;
}

function requireRole(role: string) {
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function(user: User, ...args: any[]) {
            if (user.role === role) {
                return originalMethod.apply(this, [user, ...args]);
            } else {
                console.log('无权限执行该方法');
            }
        };
        return descriptor;
    };
}

class AdminPanel {
    @requireRole('admin')
    deleteUser(user: User, userId: number) {
        console.log(`管理员 ${user.role} 删除用户 ${userId}`);
    }
}

const adminUser: User = { role: 'admin' };
const normalUser: User = { role: 'user' };
const panel = new AdminPanel();
panel.deleteUser(adminUser, 1); // 输出:管理员 admin 删除用户 1
panel.deleteUser(normalUser, 1); // 输出:无权限执行该方法
  • 缓存结果:对于一些计算量较大的方法,可以缓存其结果,避免重复计算。
function cache(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const cache = new Map();
    descriptor.value = function(...args: any[]) {
        const key = args.toString();
        if (cache.has(key)) {
            return cache.get(key);
        }
        const result = originalMethod.apply(this, args);
        cache.set(key, result);
        return result;
    };
    return descriptor;
}

class Fibonacci {
    @cache
    fib(n: number) {
        if (n <= 1) {
            return n;
        }
        return this.fib(n - 1) + this.fib(n - 2);
    }
}

const fibonacci = new Fibonacci();
fibonacci.fib(10); // 第一次计算,可能花费一些时间
fibonacci.fib(10); // 从缓存中获取结果,速度更快

四、属性装饰器

4.1 属性装饰器的原理

属性装饰器应用于类的属性声明之前,它接收两个参数:

  • target:对于静态属性,它是类的构造函数;对于实例属性,它是类的原型对象。
  • propertyKey:属性的名称。 属性装饰器不能直接修改属性的值,但可以通过在目标对象上添加元数据来间接影响属性的行为。
function propertyDecorator(target: any, propertyKey: string) {
    let value: any;
    const getter = function() {
        return value;
    };
    const setter = function(newValue: any) {
        if (typeof newValue === 'number' && newValue > 0) {
            value = newValue;
        } else {
            console.log('值必须是大于0的数字');
        }
    };
    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
        enumerable: true,
        configurable: true
    });
}

class Product {
    @propertyDecorator
    price: number;
}

const product = new Product();
product.price = 10; // 合法赋值
product.price = -5; // 输出:值必须是大于0的数字
console.log(product.price); // 输出:10

在上述代码中,propertyDecorator装饰器通过Object.defineProperty方法为price属性定义了自定义的gettersetter,从而对属性值的设置进行了限制。

4.2 属性装饰器的应用场景

  • 数据验证:确保属性值符合特定的格式或范围。
function validateEmail(target: any, propertyKey: string) {
    let value: string;
    const getter = function() {
        return value;
    };
    const setter = function(newValue: string) {
        const emailRegex = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;
        if (emailRegex.test(newValue)) {
            value = newValue;
        } else {
            console.log('不是有效的邮箱地址');
        }
    };
    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
        enumerable: true,
        configurable: true
    });
}

class UserProfile {
    @validateEmail
    email: string;
}

const userProfile = new UserProfile();
userProfile.email = 'test@example.com'; // 合法赋值
userProfile.email = 'invalid-email'; // 输出:不是有效的邮箱地址
console.log(userProfile.email); // 输出:test@example.com
  • 属性标记:通过添加元数据标记属性,以便在后续代码中进行特定处理。
function markAsImportant(target: any, propertyKey: string) {
    Reflect.defineMetadata('isImportant', true, target, propertyKey);
}

class Task {
    @markAsImportant
    title: string;

    constructor(title: string) {
        this.title = title;
    }
}

const task = new Task('完成项目文档');
const isImportant = Reflect.getMetadata('isImportant', task, 'title');
console.log(isImportant); // 输出:true

五、参数装饰器

5.1 参数装饰器的原理

参数装饰器应用于类的方法参数声明之前,它接收三个参数:

  • target:对于静态方法,它是类的构造函数;对于实例方法,它是类的原型对象。
  • propertyKey:方法的名称。
  • parameterIndex:参数在方法参数列表中的索引位置。 参数装饰器通常用于获取参数的元数据,或根据参数的位置进行特定的操作。
function parameterDecorator(target: any, propertyKey: string, parameterIndex: number) {
    let metadata = Reflect.getMetadata('parameters', target, propertyKey) || [];
    metadata.push({ index: parameterIndex, type: 'custom' });
    Reflect.defineMetadata('parameters', metadata, target, propertyKey);
}

class MyService {
    myMethod(@parameterDecorator param: string) {
        // 方法实现
    }
}

const metadata = Reflect.getMetadata('parameters', MyService.prototype,'myMethod');
console.log(metadata); 
// 输出:[{ index: 0, type: 'custom' }]

在上述代码中,parameterDecorator装饰器为myMethod方法的参数添加了元数据,通过Reflect API可以获取这些元数据。

5.2 参数装饰器的应用场景

  • 参数验证:根据参数的位置对参数进行验证。
function validateNumber(target: any, propertyKey: string, parameterIndex: number) {
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function(...args: any[]) {
            if (typeof args[parameterIndex] === 'number') {
                return originalMethod.apply(this, args);
            } else {
                console.log(`参数 ${parameterIndex} 必须是数字`);
            }
        };
        return descriptor;
    };
}

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

const calculator = new Calculator();
calculator.add(2, 3); // 输出:5
calculator.add('2', 3); // 输出:参数 0 必须是数字
  • 依赖注入:根据参数的元数据进行依赖注入。
interface Logger {
    log(message: string): void;
}

class ConsoleLogger implements Logger {
    log(message: string) {
        console.log(message);
    }
}

function injectLogger(target: any, propertyKey: string, parameterIndex: number) {
    Reflect.defineMetadata('injectLogger', true, target, propertyKey);
}

class MyClass {
    myMethod(@injectLogger logger: Logger) {
        logger.log('这是一条日志');
    }
}

function resolveDependencies(target: any) {
    const methods = Object.getOwnPropertyNames(target.prototype);
    methods.forEach(method => {
        if (Reflect.getMetadata('injectLogger', target.prototype, method)) {
            const originalMethod = target.prototype[method];
            target.prototype[method] = function() {
                const logger = new ConsoleLogger();
                return originalMethod.apply(this, [logger, ...arguments]);
            };
        }
    });
}

resolveDependencies(MyClass);
const myObject = new MyClass();
myObject.myMethod(); 
// 输出:这是一条日志

六、装饰器组合与执行顺序

6.1 装饰器组合

在TypeScript中,可以对一个目标应用多个装饰器。例如,对一个类可以同时应用类装饰器和属性装饰器。

function classDecorator1(target: Function) {
    console.log('类装饰器1');
    return target;
}

function classDecorator2(target: Function) {
    console.log('类装饰器2');
    return target;
}

function propertyDecorator1(target: any, propertyKey: string) {
    console.log('属性装饰器1');
}

function propertyDecorator2(target: any, propertyKey: string) {
    console.log('属性装饰器2');
}

@classDecorator1
@classDecorator2
class MyClass {
    @propertyDecorator1
    @propertyDecorator2
    myProperty: string;
}
// 输出:
// 类装饰器2
// 类装饰器1
// 属性装饰器2
// 属性装饰器1

从上述代码可以看出,类装饰器从下往上执行,而属性装饰器从上往下执行。

6.2 执行顺序规则

  • 类装饰器:多个类装饰器从下往上依次执行,最后执行的装饰器返回的结果作为类的定义。
  • 方法、属性、参数装饰器:对于这些装饰器,在同一个声明上的多个装饰器从上往下依次执行。例如,对于一个方法上的多个装饰器,先执行最上面的装饰器,再依次向下执行。

七、装饰器与元数据

7.1 元数据的概念

元数据是关于数据的数据,在TypeScript装饰器中,元数据用于为目标(类、方法、属性、参数)添加额外的信息。这些信息可以在运行时通过Reflect API进行读取和操作。

7.2 使用Reflect API操作元数据

Reflect API提供了一组方法来操作元数据。例如,Reflect.defineMetadata用于定义元数据,Reflect.getMetadata用于获取元数据。

function markAsDeprecated(target: any, propertyKey: string) {
    Reflect.defineMetadata('deprecated', true, target, propertyKey);
}

class MyClass {
    @markAsDeprecated
    oldMethod() {
        // 方法实现
    }
}

const isDeprecated = Reflect.getMetadata('deprecated', MyClass.prototype, 'oldMethod');
console.log(isDeprecated); // 输出:true

在上述代码中,markAsDeprecated装饰器使用Reflect.defineMetadataoldMethod方法添加了deprecated元数据,然后通过Reflect.getMetadata获取该元数据。

7.3 元数据在装饰器中的应用

元数据在装饰器中有广泛的应用。例如,在权限验证装饰器中,可以使用元数据标记方法所需的权限,然后在运行时根据用户的角色来检查是否有权限执行该方法。

function requireRole(role: string) {
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        Reflect.defineMetadata('requiredRole', role, target, propertyKey);
        const originalMethod = descriptor.value;
        descriptor.value = function(user: { role: string }, ...args: any[]) {
            const requiredRole = Reflect.getMetadata('requiredRole', target, propertyKey);
            if (user.role === requiredRole) {
                return originalMethod.apply(this, [user, ...args]);
            } else {
                console.log('无权限执行该方法');
            }
        };
        return descriptor;
    };
}

class AdminPanel {
    @requireRole('admin')
    deleteUser(user: { role: string }, userId: number) {
        console.log(`管理员 ${user.role} 删除用户 ${userId}`);
    }
}

const adminUser = { role: 'admin' };
const normalUser = { role: 'user' };
const panel = new AdminPanel();
panel.deleteUser(adminUser, 1); // 输出:管理员 admin 删除用户 1
panel.deleteUser(normalUser, 1); // 输出:无权限执行该方法

在上述代码中,requireRole装饰器使用元数据标记了deleteUser方法所需的角色,在方法执行时通过获取元数据来进行权限验证。

八、装饰器在实际项目中的应用案例

8.1 日志记录在Web应用中的应用

在一个Web应用中,我们可能需要对用户的操作进行日志记录。例如,记录用户登录、注销、创建资源等操作。

function logAction(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
        const action = propertyKey;
        console.log(`用户执行了 ${action} 操作`);
        const result = originalMethod.apply(this, args);
        return result;
    };
    return descriptor;
}

class UserService {
    @logAction
    login(username: string, password: string) {
        // 登录逻辑
        console.log('用户登录成功');
    }

    @logAction
    logout() {
        // 注销逻辑
        console.log('用户注销成功');
    }
}

const userService = new UserService();
userService.login('John', 'password');
// 输出:
// 用户执行了 login 操作
// 用户登录成功

userService.logout();
// 输出:
// 用户执行了 logout 操作
// 用户注销成功

通过使用装饰器,我们可以在不修改业务逻辑的基础上,方便地添加日志记录功能。

8.2 缓存机制在API服务中的应用

在API服务中,对于一些频繁调用且数据变化不频繁的接口,可以使用缓存机制来提高性能。

function cacheResponse(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const cache = new Map();
    descriptor.value = async function(...args: any[]) {
        const key = args.toString();
        if (cache.has(key)) {
            return cache.get(key);
        }
        const result = await originalMethod.apply(this, args);
        cache.set(key, result);
        return result;
    };
    return descriptor;
}

class DataService {
    @cacheResponse
    async getData() {
        // 模拟从数据库或其他数据源获取数据
        await new Promise(resolve => setTimeout(resolve, 1000));
        return { data: '一些数据' };
    }
}

const dataService = new DataService();
dataService.getData().then(data => console.log(data)); 
// 第一次调用,花费1秒左右
dataService.getData().then(data => console.log(data)); 
// 从缓存中获取,几乎立即返回

通过cacheResponse装饰器,我们为getData方法添加了缓存功能,提高了API的响应速度。

8.3 依赖注入在大型项目中的应用

在大型项目中,依赖注入是一种常见的设计模式,用于解耦组件之间的依赖关系。

interface Database {
    connect(): void;
}

class MySQLDatabase implements Database {
    connect() {
        console.log('连接到MySQL数据库');
    }
}

class PostgreSQLDatabase implements Database {
    connect() {
        console.log('连接到PostgreSQL数据库');
    }
}

function injectDatabase(target: any, propertyKey: string, parameterIndex: number) {
    Reflect.defineMetadata('injectDatabase', true, target, propertyKey);
}

class UserRepository {
    constructor(@injectDatabase private database: Database) {}

    getUser() {
        this.database.connect();
        console.log('从数据库获取用户数据');
    }
}

function resolveDatabaseDependencies(target: any) {
    const methods = Object.getOwnPropertyNames(target.prototype);
    methods.forEach(method => {
        if (Reflect.getMetadata('injectDatabase', target.prototype, method)) {
            const originalConstructor = target.prototype.constructor;
            target.prototype.constructor = function() {
                const database = new MySQLDatabase(); // 可以根据配置选择不同的数据库实现
                return originalConstructor.apply(this, [database, ...arguments]);
            };
        }
    });
}

resolveDatabaseDependencies(UserRepository);
const userRepository = new UserRepository();
userRepository.getUser();
// 输出:
// 连接到MySQL数据库
// 从数据库获取用户数据

通过使用参数装饰器和依赖注入的方式,UserRepository类不需要关心具体的数据库实现,提高了代码的可维护性和可测试性。

九、装饰器的局限性与注意事项

9.1 装饰器的局限性

  • 兼容性问题:装饰器是ES7的提案,虽然TypeScript支持,但在一些旧版本的JavaScript运行环境中可能不支持。在使用装饰器时,需要考虑项目的运行环境兼容性。
  • 调试困难:由于装饰器会对目标进行修改,在调试时可能会增加难度。例如,当一个方法被多个装饰器修改后,追踪其原始行为和装饰器的具体影响变得更加复杂。

9.2 注意事项

  • 避免过度使用:装饰器虽然强大,但过度使用可能会导致代码难以理解和维护。应谨慎使用装饰器,确保其使用是为了提高代码的可维护性和可扩展性,而不是增加复杂性。
  • 保持装饰器的单一职责:每个装饰器应该专注于一个特定的功能,例如日志记录、权限验证等。这样可以提高装饰器的复用性和可维护性。
  • 注意执行顺序:在使用多个装饰器时,要清楚它们的执行顺序,避免因执行顺序不当导致的错误。例如,在依赖注入的场景中,如果装饰器执行顺序错误,可能会导致依赖无法正确注入。

在使用TypeScript装饰器时,我们需要充分了解其原理和应用场景,同时注意其局限性和注意事项,以确保在项目中能够合理、有效地使用装饰器,提升代码的质量和开发效率。