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

TypeScript属性装饰器:如何优雅地装饰类属性

2022-03-064.7k 阅读

TypeScript属性装饰器基础概念

在TypeScript中,属性装饰器是一种特殊的声明,它可以附加到类的属性声明上。属性装饰器主要用于观察、修改或替换属性的定义。

属性装饰器表达式会在运行时当作函数被调用,它接受两个参数:

  1. 对于静态成员,是类的构造函数;对于实例成员,是类的原型对象
  2. 成员的名字

下面是一个简单的属性装饰器示例:

function logProperty(target: any, propertyKey: string) {
    let value = target[propertyKey];

    const getter = () => {
        console.log(`Get: ${propertyKey} => ${value}`);
        return value;
    };

    const setter = (newValue: any) => {
        console.log(`Set: ${propertyKey} => ${newValue}`);
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class Person {
    @logProperty
    name: string;

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

const person = new Person('John');
console.log(person.name);
person.name = 'Jane';

在上述代码中,logProperty是一个属性装饰器。当访问或修改Person类的name属性时,控制台会打印出相应的日志信息。

利用属性装饰器实现属性验证

属性装饰器可以用来实现属性的验证逻辑。假设我们有一个User类,其中的age属性需要在设置时进行验证,确保年龄在合理范围内。

function validateAge(target: any, propertyKey: string) {
    let value: number;

    const getter = () => value;

    const setter = (newValue: number) => {
        if (typeof newValue === 'number' && newValue >= 0 && newValue <= 120) {
            value = newValue;
        } else {
            throw new Error('Invalid age value');
        }
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class User {
    @validateAge
    age: number;

    constructor(age: number) {
        this.age = age;
    }
}

try {
    const user = new User(25);
    console.log(user.age);
    user.age = 30;
    console.log(user.age);
    user.age = 150;
} catch (error) {
    console.error(error.message);
}

在这个例子中,validateAge装饰器会在设置age属性时验证传入的值是否在0到120之间。如果超出范围,就会抛出一个错误。

实现属性的缓存

属性装饰器还可以用于实现属性的缓存。当一个属性的计算成本较高时,我们可以缓存其计算结果,避免重复计算。

function cache(target: any, propertyKey: string) {
    let cachedValue;
    const originalMethod = target[propertyKey];

    const newMethod = function() {
        if (cachedValue === undefined) {
            cachedValue = originalMethod.apply(this, arguments);
        }
        return cachedValue;
    };

    target[propertyKey] = newMethod;
}

class MathCalculations {
    @cache
    expensiveCalculation() {
        // 模拟一个昂贵的计算
        let result = 0;
        for (let i = 0; i < 1000000; i++) {
            result += i;
        }
        return result;
    }
}

const mathCalculations = new MathCalculations();
console.time('firstCall');
console.log(mathCalculations.expensiveCalculation());
console.timeEnd('firstCall');

console.time('secondCall');
console.log(mathCalculations.expensiveCalculation());
console.timeEnd('secondCall');

在上述代码中,cache装饰器会缓存expensiveCalculation方法的返回值。第一次调用时会进行实际的计算,而后续调用则直接返回缓存的值,大大提高了性能。

与元数据结合使用

TypeScript的反射机制提供了元数据(Metadata)功能,属性装饰器可以很好地与元数据结合使用。通过元数据,我们可以在运行时获取关于类、属性、方法等的额外信息。

首先,需要安装reflect - metadata库,并在项目中引入它。

npm install reflect - metadata

然后在代码中引入:

import 'reflect - metadata';

下面是一个结合元数据的示例,我们使用属性装饰器为属性添加一些自定义元数据。

const metadataKey = 'description';

function addDescription(description: string) {
    return function(target: any, propertyKey: string) {
        Reflect.defineMetadata(metadataKey, description, target, propertyKey);
    };
}

class Product {
    @addDescription('The product name')
    name: string;

    @addDescription('The product price')
    price: number;

    constructor(name: string, price: number) {
        this.name = name;
        this.price = price;
    }
}

const product = new Product('Widget', 10.99);
const nameDescription = Reflect.getMetadata(metadataKey, product, 'name');
const priceDescription = Reflect.getMetadata(metadataKey, product, 'price');

console.log(`Name description: ${nameDescription}`);
console.log(`Price description: ${priceDescription}`);

在这个例子中,addDescription装饰器使用Reflect.defineMetadata方法为Product类的nameprice属性添加了描述元数据。然后通过Reflect.getMetadata方法在运行时获取这些元数据。

条件装饰属性

有时候,我们可能需要根据某些条件来决定是否应用属性装饰器。这可以通过函数动态返回装饰器来实现。

function conditionalDecorator(condition: boolean) {
    return function(target: any, propertyKey: string) {
        if (condition) {
            // 实际的装饰器逻辑
            let value = target[propertyKey];

            const getter = () => {
                console.log(`Conditional Get: ${propertyKey} => ${value}`);
                return value;
            };

            const setter = (newValue: any) => {
                console.log(`Conditional Set: ${propertyKey} => ${newValue}`);
                value = newValue;
            };

            if (delete target[propertyKey]) {
                Object.defineProperty(target, propertyKey, {
                    get: getter,
                    set: setter,
                    enumerable: true,
                    configurable: true
                });
            }
        }
    };
}

class SomeClass {
    @conditionalDecorator(true)
    someProperty: string;

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

const someObject = new SomeClass('example');
console.log(someObject.someProperty);
someObject.someProperty = 'new value';

在上述代码中,conditionalDecorator接受一个条件参数condition。如果条件为真,就会应用实际的装饰器逻辑,对属性的访问和设置进行日志记录。

装饰器工厂函数

属性装饰器工厂函数是一个返回属性装饰器的函数。通过这种方式,我们可以根据不同的参数创建不同的装饰器。

function logValue(prefix: string) {
    return function(target: any, propertyKey: string) {
        let value = target[propertyKey];

        const getter = () => {
            console.log(`${prefix} Get: ${propertyKey} => ${value}`);
            return value;
        };

        const setter = (newValue: any) => {
            console.log(`${prefix} Set: ${propertyKey} => ${newValue}`);
            value = newValue;
        };

        if (delete target[propertyKey]) {
            Object.defineProperty(target, propertyKey, {
                get: getter,
                set: setter,
                enumerable: true,
                configurable: true
            });
        }
    };
}

class AnotherClass {
    @logValue('Custom prefix: ')
    anotherProperty: string;

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

const anotherObject = new AnotherClass('hello');
console.log(anotherObject.anotherProperty);
anotherObject.anotherProperty = 'world';

在这个例子中,logValue是一个装饰器工厂函数,它接受一个prefix参数。返回的装饰器会在日志中添加这个前缀。

装饰器的执行顺序

当一个属性上有多个装饰器时,它们的执行顺序是从最靠近属性声明的装饰器开始,自下而上执行。

function firstDecorator(target: any, propertyKey: string) {
    console.log('First decorator applied');
}

function secondDecorator(target: any, propertyKey: string) {
    console.log('Second decorator applied');
}

class MultipleDecoratorsClass {
    @firstDecorator
    @secondDecorator
    multiDecoratedProperty: string;

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

const multiDecoratedObject = new MultipleDecoratorsClass('test');

在上述代码中,secondDecorator先被应用,然后是firstDecorator。控制台会先打印Second decorator applied,然后是First decorator applied

在类继承中的属性装饰器

当一个类继承自另一个类时,属性装饰器在继承体系中的表现值得关注。如果父类的属性被装饰,子类继承该属性时,装饰器逻辑同样会生效。

function parentLogProperty(target: any, propertyKey: string) {
    let value = target[propertyKey];

    const getter = () => {
        console.log(`Parent Get: ${propertyKey} => ${value}`);
        return value;
    };

    const setter = (newValue: any) => {
        console.log(`Parent Set: ${propertyKey} => ${newValue}`);
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class ParentClass {
    @parentLogProperty
    parentProperty: string;

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

class ChildClass extends ParentClass {
    constructor(parentProperty: string) {
        super(parentProperty);
    }
}

const child = new ChildClass('from child');
console.log(child.parentProperty);
child.parentProperty = 'new value from child';

在这个例子中,ParentClassparentProperty属性被parentLogProperty装饰器装饰。ChildClass继承自ParentClass,当访问或修改child.parentProperty时,同样会触发父类中装饰器定义的日志记录逻辑。

与类装饰器的配合使用

属性装饰器可以与类装饰器一起使用,以实现更强大的功能。类装饰器可以对整个类进行修改,而属性装饰器专注于类的属性。

function classLogger(target: Function) {
    return class extends target {
        constructor(...args: any[]) {
            console.log('Class instantiated');
            super(...args);
        }
    };
}

function propertyLogger(target: any, propertyKey: string) {
    let value = target[propertyKey];

    const getter = () => {
        console.log(`Get: ${propertyKey} => ${value}`);
        return value;
    };

    const setter = (newValue: any) => {
        console.log(`Set: ${propertyKey} => ${newValue}`);
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

@classLogger
class CombinedClass {
    @propertyLogger
    combinedProperty: string;

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

const combinedObject = new CombinedClass('combined value');
console.log(combinedObject.combinedProperty);
combinedObject.combinedProperty = 'new combined value';

在上述代码中,classLogger是一个类装饰器,它在类实例化时打印日志。propertyLogger是一个属性装饰器,用于记录属性的访问和设置。通过这种配合,我们可以在类和属性层面都添加额外的行为。

注意事项和限制

  1. 装饰器执行时机:装饰器在类定义时就会执行,而不是在实例化时。这意味着装饰器逻辑中的副作用(如日志记录、修改全局状态等)会在代码加载阶段就发生。
  2. 兼容性:虽然TypeScript支持装饰器,但它们是一项实验性特性,不同的运行环境(如浏览器、Node.js)可能对其支持程度有所不同。在使用装饰器时,需要确保目标环境能够正确运行相关代码,通常可以通过转译工具(如Babel)来提高兼容性。
  3. 调试难度:由于装饰器逻辑相对独立且在类定义阶段执行,调试装饰器相关的问题可能会比普通代码更具挑战性。建议在装饰器中添加详细的日志记录,以便在出现问题时能够快速定位。

通过深入理解和合理运用TypeScript的属性装饰器,我们可以使代码更加优雅、可维护和功能强大。无论是实现属性验证、缓存,还是与元数据结合等,属性装饰器都为前端开发带来了更多的可能性和灵活性。在实际项目中,根据具体需求选择合适的装饰器应用场景,可以有效提升代码质量和开发效率。