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

TypeScript类装饰器:@decorator的使用解析

2021-05-052.1k 阅读

TypeScript类装饰器基础概念

在TypeScript中,装饰器是一种特殊类型的声明,它能够附加到类声明、方法、访问器、属性或参数上,为这些元素添加额外的行为或元数据。类装饰器是应用于类构造函数的装饰器,其作用是对类本身进行修改,比如添加新的属性、方法,修改类的行为等。

类装饰器的写法以@符号开头,紧接着是装饰器函数名,它会在类定义被评估时立即执行。装饰器函数接受一个参数,即被装饰类的构造函数。

下面来看一个简单的示例:

function classDecorator(target: Function) {
    target.prototype.newProperty = "This is a new property added by the decorator";
}

@classDecorator
class MyClass { }

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

在上述代码中,classDecorator是一个类装饰器,它接受MyClass的构造函数作为参数。在装饰器函数内部,我们为MyClass的原型添加了一个新属性newProperty。当我们创建MyClass的实例myObject后,就可以访问这个新添加的属性。

类装饰器的返回值

类装饰器函数可以返回一个值,这个返回值可以是一个新的构造函数,也可以是对原构造函数的修改。如果返回一个新的构造函数,那么这个新的构造函数将替代原有的类构造函数。

function classDecorator(target: Function) {
    return class extends target {
        newMethod() {
            return "This is a new method in the modified class";
        }
    };
}

@classDecorator
class MyClass { }

const myObject = new MyClass();
console.log((myObject as any).newMethod()); 

在这个例子中,classDecorator返回了一个新的类,这个类继承自原MyClass,并添加了一个新方法newMethod。通过类型断言,我们可以调用这个新方法。

多个类装饰器的应用

TypeScript支持在一个类上应用多个装饰器。这些装饰器会按照从顶到下的顺序依次执行,每个装饰器的返回值会作为下一个装饰器的参数。

function firstDecorator(target: Function) {
    return class extends target {
        firstDecoratorMessage = "This is from the first decorator";
    };
}

function secondDecorator(target: Function) {
    return class extends target {
        secondDecoratorMessage = "This is from the second decorator";
    };
}

@firstDecorator
@secondDecorator
class MyClass { }

const myObject = new MyClass();
console.log((myObject as any).firstDecoratorMessage); 
console.log((myObject as any).secondDecoratorMessage); 

在上述代码中,secondDecorator先执行,它返回的类作为firstDecorator的参数。最终MyClass的实例myObject可以访问两个装饰器添加的属性。

装饰器工厂

装饰器工厂是一个函数,它返回一个装饰器。通过使用装饰器工厂,我们可以在应用装饰器时传递参数。

function classDecoratorFactory(param: string) {
    return function (target: Function) {
        target.prototype.decoratorParam = param;
    };
}

@classDecoratorFactory("This is a parameter")
class MyClass { }

const myObject = new MyClass();
console.log((myObject as any).decoratorParam); 

在这个例子中,classDecoratorFactory是一个装饰器工厂,它接受一个字符串参数param。当我们使用@classDecoratorFactory("This is a parameter")时,实际上是先调用classDecoratorFactory函数并传入参数,返回的真正装饰器函数再应用到MyClass上。

类装饰器与依赖注入

依赖注入是一种软件设计模式,它允许将依赖关系从一个组件传递到另一个组件,而不是在组件内部创建依赖。类装饰器可以在依赖注入场景中发挥重要作用。

假设我们有一个服务类UserService,它提供用户相关的功能:

class UserService {
    getUser() {
        return { name: "John Doe" };
    }
}

我们可以使用类装饰器来注入这个服务到其他类中:

function injectUserService(target: Function) {
    const originalConstructor = target;
    return class extends originalConstructor {
        userService: UserService;
        constructor(...args: any[]) {
            super(...args);
            this.userService = new UserService();
        }
    };
}

@injectUserService
class MyComponent {
    constructor() { }
    displayUser() {
        const user = this.userService.getUser();
        console.log(`User: ${user.name}`);
    }
}

const myComponent = new MyComponent();
myComponent.displayUser(); 

在上述代码中,injectUserService装饰器为MyComponent类注入了UserService实例。MyComponent类在构造函数中创建了UserService的实例,并可以在displayUser方法中使用它。

类装饰器在AOP(面向切面编程)中的应用

面向切面编程是一种编程范式,它允许将横切关注点(如日志记录、性能监测等)从业务逻辑中分离出来。类装饰器可以很好地用于实现AOP。

以日志记录为例,我们可以创建一个类装饰器来记录类中所有方法的调用信息:

function logMethodCalls(target: Function) {
    const originalMethods = target.prototype;
    for (const methodName in originalMethods) {
        if (typeof originalMethods[methodName] === "function") {
            const originalMethod = originalMethods[methodName];
            originalMethods[methodName] = function (...args: any[]) {
                console.log(`Calling method ${methodName} with arguments:`, args);
                const result = originalMethod.apply(this, args);
                console.log(`Method ${methodName} returned:`, result);
                return result;
            };
        }
    }
    return target;
}

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

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

在这个例子中,logMethodCalls装饰器遍历MathOperations类原型上的所有方法,并为每个方法添加了日志记录功能。当调用add方法时,会先打印方法调用信息,执行完方法后再打印返回值信息。

类装饰器与元数据

TypeScript提供了reflect - metadata库来支持元数据的操作。元数据是关于数据的数据,我们可以使用类装饰器来附加元数据到类上。

首先,安装reflect - metadata库:

npm install reflect - metadata

然后在代码中使用:

import "reflect - metadata";

const METADATA_KEY = "my:metadata";

function addMetadata(target: Function) {
    Reflect.defineMetadata(METADATA_KEY, { message: "This is metadata attached by the decorator" }, target);
    return target;
}

@addMetadata
class MyClass { }

const metadata = Reflect.getMetadata(METADATA_KEY, MyClass);
console.log(metadata); 

在上述代码中,addMetadata装饰器使用Reflect.defineMetadata方法为MyClass类附加了元数据。然后通过Reflect.getMetadata方法获取并打印这些元数据。

类装饰器的兼容性与注意事项

  1. 兼容性:类装饰器是ES装饰器提案的一部分,目前在TypeScript中是实验性特性。需要在tsconfig.json文件中开启experimentalDecorators选项才能使用。
  2. 执行顺序:当多个装饰器应用于一个类时,要注意它们的执行顺序。从顶到下依次执行,且前一个装饰器的返回值会作为下一个装饰器的参数。
  3. 类型兼容性:如果装饰器返回一个新的构造函数,要注意类型兼容性。可能需要使用类型断言来访问新添加的属性或方法。
  4. 副作用:装饰器会在类定义被评估时立即执行,所以要避免在装饰器中执行有副作用的操作,除非你明确知道这些操作的影响。

结合装饰器实现数据验证

在前端开发中,数据验证是非常重要的环节。我们可以利用类装饰器来实现数据验证功能。假设我们有一个用户注册的场景,需要对用户输入的数据进行验证。

首先,定义一些验证规则的装饰器:

function required(target: Object, propertyKey: string) {
    let value: any;
    const getter = () => value;
    const setter = (newValue: any) => {
        if (newValue === undefined || newValue === null || newValue === "") {
            throw new Error(`${propertyKey} is required`);
        }
        value = newValue;
    };
    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

function minLength(length: number) {
    return function (target: Object, propertyKey: string) {
        let value: any;
        const getter = () => value;
        const setter = (newValue: any) => {
            if (typeof newValue === "string" && newValue.length < length) {
                throw new Error(`${propertyKey} must be at least ${length} characters long`);
            }
            value = newValue;
        };
        if (delete target[propertyKey]) {
            Object.defineProperty(target, propertyKey, {
                get: getter,
                set: setter,
                enumerable: true,
                configurable: true
            });
        }
    };
}

然后,定义一个用户类并应用这些装饰器:

class User {
    @required
    @minLength(3)
    username: string;

    @required
    password: string;

    constructor(username: string, password: string) {
        this.username = username;
        this.password = password;
    }
}

try {
    const user = new User("abc", "123456");
    console.log("User created successfully:", user);
} catch (error) {
    console.error("Error creating user:", error.message);
}

在上述代码中,required装饰器确保属性不为空,minLength装饰器确保字符串属性达到指定的最小长度。当创建User实例时,如果不符合验证规则,就会抛出错误。

类装饰器在路由中的应用

在前端框架如Angular中,路由是应用的重要组成部分。我们可以使用类装饰器来定义路由相关的信息。

假设我们有一个简单的路由装饰器示例:

interface Route {
    path: string;
    component: any;
}

const routes: Route[] = [];

function RouteConfig(path: string) {
    return function (target: Function) {
        routes.push({ path, component: target });
    };
}

@RouteConfig("/home")
class HomeComponent { }

@RouteConfig("/about")
class AboutComponent { }

console.log("Routes:", routes); 

在这个例子中,RouteConfig装饰器用于为组件定义路由路径。每个被RouteConfig装饰的类都会被添加到routes数组中,记录其路径和对应的组件。这样可以方便地管理和维护应用的路由配置。

类装饰器在状态管理中的应用

在前端应用中,状态管理也是一个关键部分。例如在Vuex或Redux中,我们可以使用类装饰器来简化状态管理的代码。

以一个简单的Vuex - like状态管理为例:

interface State {
    count: number;
}

const state: State = {
    count: 0
};

function stateProp(target: Object, propertyKey: string) {
    Object.defineProperty(target, propertyKey, {
        get() {
            return state[propertyKey];
        },
        set(newValue: any) {
            state[propertyKey] = newValue;
        }
    });
}

class Counter {
    @stateProp
    count: number;

    increment() {
        this.count++;
    }
}

const counter = new Counter();
console.log("Initial count:", counter.count); 
counter.increment();
console.log("Incremented count:", counter.count); 

在上述代码中,stateProp装饰器将类的属性与全局状态state进行绑定。Counter类的count属性可以直接操作全局状态中的count值,通过increment方法修改count,也会同步更新全局状态。

类装饰器在测试中的应用

在单元测试中,类装饰器可以用来设置测试环境、添加测试钩子等。

例如,我们可以创建一个装饰器来为测试类添加一个beforeEach钩子:

function beforeEachHook(hookFunction: () => void) {
    return function (target: Function) {
        const originalConstructor = target;
        return class extends originalConstructor {
            constructor(...args: any[]) {
                super(...args);
                hookFunction();
            }
        };
    };
}

function setupTest() {
    console.log("Setting up test...");
}

@beforeEachHook(setupTest)
class MyTest {
    testMethod() {
        console.log("Running test method...");
    }
}

const testInstance = new MyTest();
testInstance.testMethod(); 

在这个例子中,beforeEachHook装饰器会在MyTest类实例化时执行setupTest函数,模拟测试前的准备工作。

类装饰器在代码复用中的应用

类装饰器可以帮助我们实现代码复用。例如,我们有多个类需要添加相同的日志记录功能,通过类装饰器可以避免在每个类中重复编写日志记录代码。

function logClass(target: Function) {
    const originalMethods = target.prototype;
    for (const methodName in originalMethods) {
        if (typeof originalMethods[methodName] === "function") {
            const originalMethod = originalMethods[methodName];
            originalMethods[methodName] = function (...args: any[]) {
                console.log(`Calling method ${methodName} of ${target.name}`);
                const result = originalMethod.apply(this, args);
                console.log(`Method ${methodName} of ${target.name} returned:`, result);
                return result;
            };
        }
    }
    return target;
}

@logClass
class Class1 {
    method1() {
        return "Result of method1";
    }
}

@logClass
class Class2 {
    method2() {
        return "Result of method2";
    }
}

const class1Instance = new Class1();
const class2Instance = new Class2();

class1Instance.method1(); 
class2Instance.method2(); 

在上述代码中,logClass装饰器为Class1Class2类都添加了日志记录功能,使得代码更加简洁和可维护。

类装饰器在性能优化中的应用

在前端性能优化方面,类装饰器也可以发挥作用。比如我们可以使用类装饰器来实现缓存机制,避免重复计算。

function cacheMethod(target: Object, 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 MathCalculations {
    @cacheMethod
    expensiveCalculation(a: number, b: number) {
        // 模拟一个耗时操作
        for (let i = 0; i < 10000000; i++);
        return a + b;
    }
}

const mathCalculations = new MathCalculations();
console.log(mathCalculations.expensiveCalculation(2, 3)); 
console.log(mathCalculations.expensiveCalculation(2, 3)); 

在这个例子中,cacheMethod装饰器为expensiveCalculation方法添加了缓存功能。第一次调用该方法时,会执行实际的计算并缓存结果,后续相同参数的调用直接从缓存中获取结果,从而提高性能。

类装饰器在安全性方面的应用

在安全性方面,类装饰器可以用于权限控制等场景。例如,我们可以创建一个装饰器来检查用户是否有权限访问类的方法。

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("Access denied");
            }
            return originalMethod.apply(this, args);
        };
        return descriptor;
    };
}

class AdminPanel {
    @requireRole("admin")
    deleteUser() {
        console.log("Deleting user...");
    }
}

const adminPanel = new AdminPanel();
try {
    adminPanel.deleteUser(); 
} catch (error) {
    console.error("Error:", error.message);
}

在上述代码中,requireRole装饰器用于检查当前用户的角色是否为admin,如果不是则抛出错误,从而限制对deleteUser方法的访问,提高应用的安全性。

通过以上详细的介绍和丰富的代码示例,我们对TypeScript类装饰器@decorator的使用有了全面且深入的理解,从基础概念到各种实际应用场景,它为前端开发带来了更多的灵活性和可维护性。在实际项目中,可以根据具体需求合理运用类装饰器,提升代码质量和开发效率。