TypeScript属性装饰器:如何优雅地装饰类属性
TypeScript属性装饰器基础概念
在TypeScript中,属性装饰器是一种特殊的声明,它可以附加到类的属性声明上。属性装饰器主要用于观察、修改或替换属性的定义。
属性装饰器表达式会在运行时当作函数被调用,它接受两个参数:
- 对于静态成员,是类的构造函数;对于实例成员,是类的原型对象。
- 成员的名字。
下面是一个简单的属性装饰器示例:
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
类的name
和price
属性添加了描述元数据。然后通过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';
在这个例子中,ParentClass
的parentProperty
属性被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
是一个属性装饰器,用于记录属性的访问和设置。通过这种配合,我们可以在类和属性层面都添加额外的行为。
注意事项和限制
- 装饰器执行时机:装饰器在类定义时就会执行,而不是在实例化时。这意味着装饰器逻辑中的副作用(如日志记录、修改全局状态等)会在代码加载阶段就发生。
- 兼容性:虽然TypeScript支持装饰器,但它们是一项实验性特性,不同的运行环境(如浏览器、Node.js)可能对其支持程度有所不同。在使用装饰器时,需要确保目标环境能够正确运行相关代码,通常可以通过转译工具(如Babel)来提高兼容性。
- 调试难度:由于装饰器逻辑相对独立且在类定义阶段执行,调试装饰器相关的问题可能会比普通代码更具挑战性。建议在装饰器中添加详细的日志记录,以便在出现问题时能够快速定位。
通过深入理解和合理运用TypeScript的属性装饰器,我们可以使代码更加优雅、可维护和功能强大。无论是实现属性验证、缓存,还是与元数据结合等,属性装饰器都为前端开发带来了更多的可能性和灵活性。在实际项目中,根据具体需求选择合适的装饰器应用场景,可以有效提升代码质量和开发效率。