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

属性装饰器:优化TypeScript类的属性管理

2024-10-122.5k 阅读

TypeScript属性装饰器基础

属性装饰器是TypeScript提供的一种元编程特性,它允许我们在类的属性定义上附加额外的行为或元数据。属性装饰器的语法非常简洁,它紧跟在属性声明之前,由一个@符号和装饰器函数名组成。例如:

class MyClass {
    @logProperty
    myProperty: string;
}

这里@logProperty就是一个属性装饰器,它应用在myClass类的myProperty属性上。

属性装饰器函数接收两个参数:

  1. 对于静态成员:它接收目标类的构造函数作为第一个参数。
  2. 对于实例成员:它接收目标类的原型对象作为第一个参数。
  3. 第二个参数:始终是属性的名称(字符串形式)。

下面是一个简单的属性装饰器示例,它记录属性被访问的时间:

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

    const getter = function () {
        console.log(`${new Date()} - Accessed ${propertyKey}`);
        return value;
    };

    const setter = function (newValue: any) {
        console.log(`${new Date()} - Set ${propertyKey} to ${newValue}`);
        value = newValue;
    };

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

class MyClass {
    @logProperty
    myProperty: string = 'initial value';
}

const myObj = new MyClass();
console.log(myObj.myProperty);
myObj.myProperty = 'new value';

在这个例子中,logProperty装饰器通过Object.defineProperty重新定义了属性的存取器,在访问和设置属性时记录时间信息。

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

属性装饰器在实现属性验证方面非常有用。假设我们有一个用户类,其中的age属性应该是一个正整数。我们可以使用属性装饰器来确保这一点。

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

    const getter = function () {
        return value;
    };

    const setter = function (newValue: number) {
        if (typeof newValue!== 'number' || newValue <= 0) {
            throw new Error(`${propertyKey} must be a positive number`);
        }
        value = newValue;
    };

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

class User {
    @validatePositiveNumber
    age: number;

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

try {
    const user = new User(25);
    console.log(user.age);

    const invalidUser = new User(-5);
} catch (error) {
    console.error(error.message);
}

在上述代码中,validatePositiveNumber装饰器检查设置的age值是否为正整数。如果不是,就抛出一个错误。这样可以有效地保证类的属性值始终处于一个合理的范围内。

结合属性装饰器和类型元数据

TypeScript允许我们在属性装饰器中结合类型元数据,这在进行更复杂的验证或依赖注入时非常有帮助。例如,我们可以使用反射机制来获取属性的类型信息。

首先,我们需要安装reflect - metadata库,因为TypeScript本身并没有内置反射功能。

npm install reflect - metadata

然后在代码中引入:

import 'reflect - metadata';

假设我们有一个简单的依赖注入场景,我们希望根据属性的类型自动注入相应的实例。

const TYPE_METADATA_KEY = 'design:type';

function inject(target: any, propertyKey: string) {
    const type = Reflect.getMetadata(TYPE_METADATA_KEY, target, propertyKey);
    let value: any;

    const getter = function () {
        if (!value) {
            value = new type();
        }
        return value;
    };

    const setter = function (newValue: any) {
        value = newValue;
    };

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

class Logger {
    log(message: string) {
        console.log(`[${new Date()}] - ${message}`);
    }
}

class MyService {
    @inject
    logger: Logger;

    doWork() {
        this.logger.log('Working...');
    }
}

const service = new MyService();
service.doWork();

在这个例子中,inject装饰器利用reflect - metadata库提供的Reflect.getMetadata方法获取属性的类型元数据。当属性被访问时,如果还没有实例化,就根据类型创建一个新的实例。这样我们就实现了简单的依赖注入功能,通过属性装饰器和类型元数据的结合,使得代码更加模块化和可维护。

实现只读属性

有时候我们希望类的某些属性是只读的,即一旦设置后就不能再修改。属性装饰器可以很方便地实现这一点。

function readonly(target: any, propertyKey: string) {
    const originalDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);

    const newDescriptor: PropertyDescriptor = {
       ...originalDescriptor,
        set: function () {
            throw new Error(`Cannot set ${propertyKey}. It is read - only.`);
        }
    };

    Object.defineProperty(target, propertyKey, newDescriptor);
}

class ImmutableObject {
    @readonly
    value: number;

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

const obj = new ImmutableObject(42);
console.log(obj.value);
try {
    obj.value = 100;
} catch (error) {
    console.error(error.message);
}

在上述代码中,readonly装饰器通过修改属性的描述符,使得属性的set方法抛出一个错误,从而阻止属性被修改。这样就实现了只读属性的功能,保证了数据的一致性和安全性。

缓存属性值

在一些情况下,属性的计算可能比较耗时,我们希望对其值进行缓存,以提高性能。属性装饰器可以很好地解决这个问题。

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

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

    target[propertyKey] = newMethod;
}

class ExpensiveCalculation {
    @cacheProperty
    calculate() {
        // 模拟一个耗时的计算
        let result = 0;
        for (let i = 0; i < 1000000; i++) {
            result += i;
        }
        return result;
    }
}

const calculation = new ExpensiveCalculation();
console.time('first call');
console.log(calculation.calculate());
console.timeEnd('first call');

console.time('second call');
console.log(calculation.calculate());
console.timeEnd('second call');

在这个例子中,cacheProperty装饰器缓存了calculate方法的返回值。第一次调用calculate方法时,会执行实际的计算并缓存结果。后续调用时,直接返回缓存的值,大大提高了性能。

处理属性的默认值

属性装饰器也可以用于处理属性的默认值。有时候我们希望在属性被访问时,如果还没有设置值,就使用一个默认值。

function setDefaultValue(defaultValue: any) {
    return function (target: any, propertyKey: string) {
        let value: any;

        const getter = function () {
            if (value === undefined) {
                value = defaultValue;
            }
            return value;
        };

        const setter = function (newValue: any) {
            value = newValue;
        };

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

class Settings {
    @setDefaultValue('default value')
    option: string;
}

const settings = new Settings();
console.log(settings.option);
settings.option = 'new value';
console.log(settings.option);

在上述代码中,setDefaultValue装饰器接受一个默认值作为参数。当属性被访问且值为undefined时,会返回默认值。这样可以确保属性始终有一个初始值,避免在使用过程中出现undefined错误。

多个属性装饰器的组合使用

在实际应用中,我们可能需要对一个属性应用多个装饰器。例如,我们可能既希望属性是只读的,又希望在访问属性时记录日志。

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

    const getter = function () {
        console.log(`${new Date()} - Accessed ${propertyKey}`);
        return value;
    };

    const setter = function (newValue: any) {
        console.log(`${new Date()} - Set ${propertyKey} to ${newValue}`);
        value = newValue;
    };

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

function readonly(target: any, propertyKey: string) {
    const originalDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);

    const newDescriptor: PropertyDescriptor = {
       ...originalDescriptor,
        set: function () {
            throw new Error(`Cannot set ${propertyKey}. It is read - only.`);
        }
    };

    Object.defineProperty(target, propertyKey, newDescriptor);
}

class MyClass {
    @logProperty
    @readonly
    myProperty: string = 'initial value';
}

const myObj = new MyClass();
console.log(myObj.myProperty);
try {
    myObj.myProperty = 'new value';
} catch (error) {
    console.error(error.message);
}

在这个例子中,myProperty属性先应用了logProperty装饰器,然后应用了readonly装饰器。这样既实现了属性访问的日志记录,又保证了属性的只读性。需要注意的是,装饰器的应用顺序是从下往上的,即离属性最近的装饰器先执行。

在继承体系中使用属性装饰器

属性装饰器在继承体系中也能很好地工作。当一个类继承自另一个带有属性装饰器的类时,装饰器的行为会被继承。

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

    const getter = function () {
        console.log(`${new Date()} - Accessed ${propertyKey}`);
        return value;
    };

    const setter = function (newValue: any) {
        console.log(`${new Date()} - Set ${propertyKey} to ${newValue}`);
        value = newValue;
    };

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

class BaseClass {
    @logProperty
    baseProperty: string;
}

class SubClass extends BaseClass {
    subProperty: string;
}

const subObj = new SubClass();
subObj.baseProperty = 'from sub - class';
console.log(subObj.baseProperty);

在上述代码中,SubClass继承自BaseClassBaseClassbaseProperty属性应用了logProperty装饰器。当在SubClass的实例中访问和设置baseProperty时,仍然会触发logProperty装饰器的日志记录行为。这使得在继承体系中,我们可以统一管理属性的行为,提高代码的复用性和可维护性。

注意事项和限制

  1. 兼容性:属性装饰器是ES6装饰器提案的一部分,但目前还没有完全标准化。在不同的运行环境中,可能需要特定的转译设置。例如,在使用TypeScript编译时,需要设置experimentalDecoratorsemitDecoratorMetadata编译选项。

  2. 性能影响:虽然属性装饰器可以实现很多强大的功能,但在某些情况下,尤其是频繁访问的属性上使用装饰器,可能会带来一定的性能开销。例如,缓存属性值的装饰器会增加额外的内存开销,需要权衡使用场景。

  3. 调试困难:由于装饰器改变了属性的默认行为,在调试过程中可能会遇到一些困难。如果属性的行为不符合预期,需要仔细检查装饰器的逻辑。

  4. 不能直接在属性装饰器中使用this:在属性装饰器函数中,this的指向可能并不是我们期望的类实例。如果需要访问类的实例成员,通常需要通过闭包或者其他方式来间接实现。

与其他设计模式的结合

  1. 单例模式:结合属性装饰器和单例模式可以实现单例属性。例如,我们可以通过属性装饰器确保某个属性在类的所有实例中共享同一个对象。
function singletonProperty(target: any, propertyKey: string) {
    let instance: any;

    const getter = function () {
        if (!instance) {
            instance = new (target.constructor as any)();
        }
        return instance;
    };

    const setter = function () {
        // 不允许设置新值,保持单例
    };

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

class SharedResource {
    data: string;

    constructor() {
        this.data = 'Shared data';
    }
}

class MyApp {
    @singletonProperty
    shared: SharedResource;
}

const app1 = new MyApp();
const app2 = new MyApp();
console.log(app1.shared === app2.shared); // true
  1. 代理模式:属性装饰器可以用于实现代理模式。例如,我们可以通过装饰器为属性创建一个代理对象,在访问属性时执行一些额外的逻辑,如权限检查等。
function proxyProperty(target: any, propertyKey: string) {
    let realValue: any;

    const getter = function () {
        // 权限检查
        if (!hasPermission(this)) {
            throw new Error('Access denied');
        }
        return realValue;
    };

    const setter = function (newValue: any) {
        if (!hasPermission(this)) {
            throw new Error('Access denied');
        }
        realValue = newValue;
    };

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

function hasPermission(instance: any) {
    // 这里可以实现具体的权限检查逻辑
    return true;
}

class SecureObject {
    @proxyProperty
    sensitiveData: string;
}

const secureObj = new SecureObject();
secureObj.sensitiveData = 'confidential';
console.log(secureObj.sensitiveData);

通过将属性装饰器与其他设计模式结合,可以进一步扩展类的功能,提高代码的灵活性和可维护性。

总结与展望

属性装饰器是TypeScript中一个非常强大的特性,它为我们提供了一种优雅的方式来管理类的属性。通过属性装饰器,我们可以实现属性验证、只读属性、缓存属性值、依赖注入等多种功能。在实际项目中,合理使用属性装饰器可以提高代码的质量和可维护性。

随着JavaScript和TypeScript的不断发展,装饰器提案也可能会进一步完善和标准化。未来,属性装饰器可能会在更多的场景中得到应用,例如在前端框架、后端服务等领域。我们需要不断探索和学习,充分发挥属性装饰器的潜力,为我们的软件开发带来更多的便利和创新。同时,在使用属性装饰器时,要注意其兼容性、性能影响和调试等方面的问题,确保代码的稳定性和可靠性。