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

TypeScript装饰器的执行顺序及相关规则

2022-02-134.5k 阅读

TypeScript 装饰器基础

在深入探讨 TypeScript 装饰器的执行顺序及相关规则之前,我们先来回顾一下装饰器的基本概念。装饰器是一种特殊类型的声明,它能够附加到类声明、方法、访问器、属性或参数上,为这些目标添加额外的行为或元数据。

TypeScript 装饰器本质上是一个函数,这个函数会在运行时被调用,接收目标(如类、方法等)作为参数,并可以返回一个新的目标或者对原目标进行修改。

类装饰器

类装饰器应用于类的定义。它接收一个参数,即被装饰的类的构造函数。例如:

function classDecorator(target: Function) {
    console.log('类装饰器被调用,目标类:', target);
    return class extends target {
        newProperty = '新属性';
        constructor(...args: any[]) {
            super(...args);
            console.log('在修改后的类构造函数中');
        }
    };
}

@classDecorator
class MyClass {
    constructor() {
        console.log('原始类构造函数');
    }
}

const myObj = new MyClass();
console.log(myObj.newProperty);

在上述代码中,classDecorator 是一个类装饰器。当 MyClass 被定义时,classDecorator 会被调用,并且它返回了一个新的类,这个新类继承自原始的 MyClass 并添加了一个新属性 newProperty

方法装饰器

方法装饰器应用于类的方法。它接收三个参数:目标对象(类的原型对象)、方法名称和描述符(包含方法的一些元数据,如 valuewritableenumerableconfigurable)。例如:

function methodDecorator(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('方法装饰器被调用,目标对象:', target);
    console.log('方法名:', propertyKey);
    console.log('描述符:', descriptor);
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log('在方法执行前');
        const result = originalMethod.apply(this, args);
        console.log('在方法执行后');
        return result;
    };
    return descriptor;
}

class MethodDecoratorClass {
    @methodDecorator
    myMethod() {
        console.log('执行原始方法');
    }
}

const methodObj = new MethodDecoratorClass();
methodObj.myMethod();

在这个例子中,methodDecorator 装饰器修改了 myMethod 的行为,在方法执行前后添加了日志输出。

属性装饰器

属性装饰器应用于类的属性。它接收两个参数:目标对象(类的原型对象)和属性名称。例如:

function propertyDecorator(target: Object, propertyKey: string) {
    console.log('属性装饰器被调用,目标对象:', target);
    console.log('属性名:', propertyKey);
    let value: any;
    const getter = function () {
        return value;
    };
    const setter = function (newValue: any) {
        console.log('设置属性值:', newValue);
        value = newValue;
    };
    Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
        enumerable: true,
        configurable: true
    });
}

class PropertyDecoratorClass {
    @propertyDecorator
    myProperty: string;
    constructor() {
        this.myProperty = '初始值';
    }
}

const propertyObj = new PropertyDecoratorClass();
console.log(propertyObj.myProperty);
propertyObj.myProperty = '新值';

在此代码中,propertyDecorator 装饰器为 myProperty 属性添加了自定义的存取器,在设置属性值时输出日志。

参数装饰器

参数装饰器应用于类方法的参数。它接收三个参数:目标对象(类的原型对象)、方法名称和参数在参数列表中的索引。例如:

function parameterDecorator(target: Object, propertyKey: string, parameterIndex: number) {
    console.log('参数装饰器被调用,目标对象:', target);
    console.log('方法名:', propertyKey);
    console.log('参数索引:', parameterIndex);
}

class ParameterDecoratorClass {
    myMethod(@parameterDecorator param: string) {
        console.log('参数值:', param);
    }
}

const parameterObj = new ParameterDecoratorClass();
parameterObj.myMethod('测试参数');

在上述代码中,parameterDecorator 装饰器在 myMethod 方法的参数上被调用,并输出了相关信息。

装饰器的执行顺序

同一类型装饰器的执行顺序

  1. 类装饰器:当有多个类装饰器应用于一个类时,它们的执行顺序是从最靠近类定义的装饰器开始,向外依次执行。例如:
function firstClassDecorator(target: Function) {
    console.log('第一个类装饰器被调用');
    return target;
}

function secondClassDecorator(target: Function) {
    console.log('第二个类装饰器被调用');
    return target;
}

@firstClassDecorator
@secondClassDecorator
class MultipleClassDecorators {
    constructor() {
        console.log('类构造函数');
    }
}

在这个例子中,secondClassDecorator 会先被调用,然后是 firstClassDecorator

  1. 方法装饰器:对于类的同一个方法上的多个方法装饰器,执行顺序也是从最靠近方法定义的装饰器开始,向外依次执行。例如:
function firstMethodDecorator(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('第一个方法装饰器被调用');
    return descriptor;
}

function secondMethodDecorator(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('第二个方法装饰器被调用');
    return descriptor;
}

class MultipleMethodDecorators {
    @firstMethodDecorator
    @secondMethodDecorator
    myMethod() {
        console.log('执行方法');
    }
}

这里,secondMethodDecorator 先被调用,接着是 firstMethodDecorator

  1. 属性装饰器:多个属性装饰器应用于同一个属性时,执行顺序同样是从最靠近属性定义的装饰器开始,向外依次执行。例如:
function firstPropertyDecorator(target: Object, propertyKey: string) {
    console.log('第一个属性装饰器被调用');
}

function secondPropertyDecorator(target: Object, propertyKey: string) {
    console.log('第二个属性装饰器被调用');
}

class MultiplePropertyDecorators {
    @firstPropertyDecorator
    @secondPropertyDecorator
    myProperty: string;
    constructor() {
        this.myProperty = '值';
    }
}

在此例中,secondPropertyDecorator 先执行,然后是 firstPropertyDecorator

  1. 参数装饰器:在同一个方法的参数上的多个参数装饰器,执行顺序是从左到右,即从参数列表的第一个参数对应的装饰器开始,依次向右执行。例如:
function firstParameterDecorator(target: Object, propertyKey: string, parameterIndex: number) {
    console.log('第一个参数装饰器被调用,参数索引:', parameterIndex);
}

function secondParameterDecorator(target: Object, propertyKey: string, parameterIndex: number) {
    console.log('第二个参数装饰器被调用,参数索引:', parameterIndex);
}

class MultipleParameterDecorators {
    myMethod(@firstParameterDecorator @secondParameterDecorator param1: string, @firstParameterDecorator @secondParameterDecorator param2: string) {
        console.log('方法参数:', param1, param2);
    }
}

const multiParamObj = new MultipleParameterDecorators();
multiParamObj.myMethod('param1 值', 'param2 值');

对于 param1firstParameterDecorator 先被调用,然后是 secondParameterDecorator;对于 param2 同样如此,整体按照从左到右的顺序。

不同类型装饰器的执行顺序

当一个类及其成员同时有不同类型的装饰器时,执行顺序遵循以下规则:

  1. 属性装饰器:首先执行类中所有属性的装饰器,按照属性在类中定义的顺序依次执行。
  2. 方法装饰器和参数装饰器:然后执行方法装饰器和参数装饰器。方法装饰器在其对应的方法定义之前执行,而参数装饰器在方法装饰器之后,方法实际调用之前执行。对于同一个方法,参数装饰器按照参数列表的顺序执行。
  3. 类装饰器:最后执行类装饰器。

例如:

function propertyDecorator(target: Object, propertyKey: string) {
    console.log('属性装饰器被调用,属性名:', propertyKey);
}

function methodDecorator(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('方法装饰器被调用,方法名:', propertyKey);
    return descriptor;
}

function parameterDecorator(target: Object, propertyKey: string, parameterIndex: number) {
    console.log('参数装饰器被调用,参数索引:', parameterIndex);
}

function classDecorator(target: Function) {
    console.log('类装饰器被调用');
    return target;
}

@classDecorator
class DifferentDecoratorsOrder {
    @propertyDecorator
    myProperty: string;

    @methodDecorator
    myMethod(@parameterDecorator param: string) {
        console.log('执行方法,参数值:', param);
    }

    constructor() {
        this.myProperty = '初始值';
    }
}

const orderObj = new DifferentDecoratorsOrder();
orderObj.myMethod('测试参数');

在上述代码中,首先 propertyDecorator 被调用,然后 methodDecorator 被调用,接着在 myMethod 调用时 parameterDecorator 被调用,最后 classDecorator 被调用。

装饰器的相关规则

装饰器工厂

装饰器工厂是一种返回装饰器函数的函数。通过使用装饰器工厂,可以在运行时动态生成装饰器。例如:

function decoratorFactory(message: string) {
    return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log('装饰器工厂生成的装饰器被调用,消息:', message);
        return descriptor;
    };
}

class DecoratorFactoryClass {
    @decoratorFactory('这是一条消息')
    myMethod() {
        console.log('执行方法');
    }
}

在这个例子中,decoratorFactory 是一个装饰器工厂,它接收一个 message 参数并返回一个装饰器函数。这个返回的装饰器函数在应用到 myMethod 时,会输出传递的消息。

装饰器组合

多个装饰器可以组合使用,以实现更复杂的功能。例如,可以同时使用方法装饰器和参数装饰器来对方法及其参数进行处理。如前面提到的在方法执行前后添加日志,同时对参数进行验证等操作。

function methodLogDecorator(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log('方法执行前');
        const result = originalMethod.apply(this, args);
        console.log('方法执行后');
        return result;
    };
    return descriptor;
}

function parameterValidateDecorator(target: Object, propertyKey: string, parameterIndex: number) {
    return function (value: any) {
        if (typeof value!=='string') {
            throw new Error('参数必须是字符串');
        }
    };
}

class DecoratorCombination {
    @methodLogDecorator
    myMethod(@parameterValidateDecorator param: string) {
        console.log('执行方法,参数值:', param);
    }
}

const combinationObj = new DecoratorCombination();
combinationObj.myMethod('测试字符串');
try {
    combinationObj.myMethod(123);
} catch (error) {
    console.error(error.message);
}

在上述代码中,methodLogDecorator 为方法添加了执行前后的日志,parameterValidateDecorator 对参数进行了类型验证。

装饰器与继承

当一个类继承自另一个带有装饰器的类时,装饰器的行为有一些特殊之处。

  1. 类装饰器:子类不会继承父类的类装饰器。每个类的类装饰器是独立应用的。例如:
function parentClassDecorator(target: Function) {
    console.log('父类装饰器被调用');
    return target;
}

function childClassDecorator(target: Function) {
    console.log('子类装饰器被调用');
    return target;
}

@parentClassDecorator
class ParentClass {
    constructor() {
        console.log('父类构造函数');
    }
}

@childClassDecorator
class ChildClass extends ParentClass {
    constructor() {
        super();
        console.log('子类构造函数');
    }
}

在这个例子中,ParentClassparentClassDecoratorChildClasschildClassDecorator 是分别独立调用的。

  1. 方法和属性装饰器:子类会继承父类方法和属性上的装饰器。例如:
function propertyDecorator(target: Object, propertyKey: string) {
    console.log('属性装饰器被调用,属性名:', propertyKey);
}

function methodDecorator(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('方法装饰器被调用,方法名:', propertyKey);
    return descriptor;
}

class ParentDecoratedClass {
    @propertyDecorator
    myProperty: string;

    @methodDecorator
    myMethod() {
        console.log('父类方法');
    }

    constructor() {
        this.myProperty = '父类属性值';
    }
}

class ChildDecoratedClass extends ParentDecoratedClass {
    constructor() {
        super();
        console.log('子类构造函数');
    }
}

const childObj = new ChildDecoratedClass();
childObj.myMethod();
console.log(childObj.myProperty);

在这个例子中,ChildDecoratedClass 继承了 ParentDecoratedClassmyPropertymyMethod 上的装饰器行为。

装饰器与元数据

装饰器可以用于添加和读取元数据。TypeScript 提供了 reflect - metadata 库来支持元数据操作。例如,可以使用装饰器为类或方法添加自定义元数据,然后在其他地方读取这些元数据。

import 'reflect - metadata';

const metadataKey = 'custom:metadata';

function addMetadataDecorator(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    Reflect.defineMetadata(metadataKey, '这是自定义元数据', target, propertyKey);
    return descriptor;
}

function readMetadataDecorator(target: Object, propertyKey: string) {
    const metadata = Reflect.getMetadata(metadataKey, target, propertyKey);
    console.log('读取到的元数据:', metadata);
}

class MetadataClass {
    @addMetadataDecorator
    myMethod() {
        console.log('执行方法');
    }

    @readMetadataDecorator
    readMetadata() {
        // 这里只是为了演示读取元数据,实际方法体可以为空
    }
}

const metadataObj = new MetadataClass();
metadataObj.myMethod();
metadataObj.readMetadata();

在上述代码中,addMetadataDecorator 使用 Reflect.defineMetadatamyMethod 添加了元数据,readMetadataDecorator 使用 Reflect.getMetadata 读取了该元数据。

装饰器的实际应用场景

日志记录

通过方法装饰器可以方便地为方法添加日志记录功能,记录方法的调用时间、参数和返回值等信息。例如:

function logMethodDecorator(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        const startTime = new Date().getTime();
        console.log(`方法 ${propertyKey} 开始调用,参数:`, args);
        const result = originalMethod.apply(this, args);
        const endTime = new Date().getTime();
        console.log(`方法 ${propertyKey} 调用结束,返回值:`, result, `,耗时:`, endTime - startTime, '毫秒');
        return result;
    };
    return descriptor;
}

class LoggerClass {
    @logMethodDecorator
    addNumbers(a: number, b: number) {
        return a + b;
    }
}

const loggerObj = new LoggerClass();
const sum = loggerObj.addNumbers(3, 5);

在这个例子中,logMethodDecorator 装饰器为 addNumbers 方法添加了详细的日志记录,方便调试和性能分析。

权限验证

在企业级应用中,权限验证是非常重要的功能。可以使用方法装饰器来实现对方法的权限验证。例如:

interface User {
    role: string;
}

const currentUser: User = { role: 'user' };

function requireRole(role: string) {
    return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            if (currentUser.role!== role) {
                throw new Error('没有权限执行此操作');
            }
            return originalMethod.apply(this, args);
        };
        return descriptor;
    };
}

class AuthorizationClass {
    @requireRole('admin')
    deleteUser() {
        console.log('删除用户操作');
    }
}

const authObj = new AuthorizationClass();
try {
    authObj.deleteUser();
} catch (error) {
    console.error(error.message);
}

在上述代码中,requireRole 装饰器工厂生成的装饰器用于验证当前用户是否具有特定角色才能执行 deleteUser 方法。

依赖注入

依赖注入是一种设计模式,通过装饰器可以方便地实现依赖注入的功能。例如:

const dependencies: { [key: string]: any } = {};

function injectDependency(dependencyName: string) {
    return function (target: Object, propertyKey: string) {
        Object.defineProperty(target, propertyKey, {
            get: function () {
                return dependencies[dependencyName];
            },
            enumerable: true,
            configurable: true
        });
    };
}

class DatabaseService {
    query() {
        return '执行数据库查询';
    }
}

class UserService {
    @injectDependency('databaseService')
    database: DatabaseService;

    getUserData() {
        return this.database.query();
    }
}

dependencies['databaseService'] = new DatabaseService();

const userServiceObj = new UserService();
const userData = userServiceObj.getUserData();
console.log(userData);

在这个例子中,injectDependency 装饰器将 databaseService 注入到 UserServicedatabase 属性中,实现了依赖注入。

装饰器使用的注意事项

装饰器的兼容性

虽然 TypeScript 支持装饰器,但它是一项实验性的特性。在不同的运行环境(如不同版本的 Node.js 或浏览器)中,装饰器的支持情况可能有所不同。例如,在某些旧版本的 Node.js 中,可能需要额外的转译配置才能正确使用装饰器。在使用装饰器时,需要确保目标运行环境对其有足够的支持。

性能影响

装饰器本质上是在运行时执行的代码,过多地使用装饰器或者在装饰器中执行复杂的操作可能会对性能产生一定的影响。例如,在方法装饰器中进行大量的计算或者频繁的日志记录,可能会导致方法执行时间变长。因此,在使用装饰器时,需要权衡其带来的便利性和性能开销。

代码可读性和维护性

当装饰器使用过多或者装饰器逻辑过于复杂时,可能会影响代码的可读性和维护性。对于不熟悉装饰器的开发人员来说,理解带有多个装饰器的代码可能会比较困难。因此,在编写装饰器时,应该尽量保持其逻辑简单明了,并且提供清晰的文档说明装饰器的功能和使用方法。

通过深入了解 TypeScript 装饰器的执行顺序及相关规则,我们可以更灵活、高效地使用装饰器来为我们的代码添加各种功能,提升代码的可维护性和可扩展性。同时,在使用过程中注意相关的注意事项,确保代码在不同环境下的稳定性和性能。