装饰器
JavaScript 装饰器:元编程的优雅实现
一、装饰器的核心概念
装饰器(Decorators)是一种特殊的声明,可用于修改类、方法、属性或参数的行为。它本质上是一个函数,接收目标对象、名称和属性描述符,并返回一个新的描述符或修改后的对象。
语法:
@decorator
class MyClass {}
// 或
class MyClass {
@decorator
myMethod() {}
}
二、装饰器的类型
JavaScript 支持以下几种装饰器:
1. 类装饰器
- 作用:修改或替换整个类的定义。
- 参数:接收类的构造函数。
示例:日志类创建
function logClass(constructor: Function) {
console.log(`Class ${constructor.name} created`);
// 可返回新的构造函数
return class extends constructor {
newMethod() {
console.log('New method added');
}
};
}
@logClass
class MyClass {
sayHello() {
console.log('Hello');
}
}
2. 方法装饰器
- 作用:修改或替换类方法的定义。
- 参数:接收目标对象、方法名和属性描述符。
示例:测量方法执行时间
function logExecutionTime(target: any, name: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`${name} executed in ${end - start}ms`);
return result;
};
return descriptor;
}
class Calculator {
@logExecutionTime
add(a: number, b: number) {
return a + b;
}
}
3. 属性装饰器
- 作用:修改类属性的行为(如 getter/setter)。
- 参数:接收目标对象和属性名。
示例:只读属性
function readonly(target: any, name: string) {
const descriptor: PropertyDescriptor = {
writable: false,
value: target[name]
};
Object.defineProperty(target, name, descriptor);
}
class Config {
@readonly
API_KEY = 'secret';
}
4. 参数装饰器
- 作用:修改方法参数的行为。
- 参数:接收目标对象、方法名和参数索引。
示例:记录参数类型
function logParameterType(target: any, methodName: string, parameterIndex: number) {
const types = Reflect.getMetadata('design:paramtypes', target, methodName);
console.log(`Parameter ${parameterIndex} of ${methodName} is: ${types[parameterIndex].name}`);
}
class UserService {
createUser(@logParameterType username: string, @logParameterType age: number) {
// ...
}
}
三、装饰器工厂(带参数的装饰器)
装饰器可以通过返回一个函数来接收参数:
示例:条件日志装饰器
function conditionalLog(enabled: boolean) {
return function(target: any, name: string, descriptor: PropertyDescriptor) {
if (!enabled) return descriptor;
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${name} with args:`, args);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class MyClass {
@conditionalLog(true)
doSomething() {
console.log('Doing something...');
}
}
四、装饰器执行顺序
- 参数装饰器 → 方法装饰器 → 属性装饰器 → 类装饰器。
- 同一类型的装饰器,从下到上(或从内到外)执行。
示例:
@classDecorator
class MyClass {
@propertyDecorator
myProp: string;
@methodDecorator
@anotherMethodDecorator // 先执行 anotherMethodDecorator
myMethod(@parameterDecorator param: any) {}
}
五、实际应用场景
1. 权限控制
function authorized(role: string) {
return function(target: any, name: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
if (this.userRole !== role) {
throw new Error('Access denied');
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class AdminPanel {
userRole = 'admin';
@authorized('admin')
deleteUser() {
// 删除用户逻辑
}
}
2. 缓存结果
function cacheResult(target: any, name: 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 MathUtils {
@cacheResult
computeFactorial(n: number) {
return n <= 1 ? 1 : n * this.computeFactorial(n - 1);
}
}
3. 错误处理
function catchError(errorHandler: (error: Error) => void) {
return function(target: any, name: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
errorHandler(error);
}
};
return descriptor;
};
}
class ApiService {
@catchError((error) => console.error('API error:', error))
async fetchData() {
const response = await fetch('https://api.example.com/data');
return response.json();
}
}
六、装饰器与 TypeScript
TypeScript 对装饰器提供了完整支持,需在 tsconfig.json
中启用:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Metadata API:
结合 reflect-metadata
库可获取类型元数据:
import 'reflect-metadata';
function logType(target: any, name: string) {
const type = Reflect.getMetadata('design:type', target, name);
console.log(`${name} type: ${type.name}`);
}
class Example {
@logType
myNumber: number;
}
七、ES 装饰器提案(Stage 3)
JavaScript 官方正在推进装饰器的标准化,语法略有变化:
// 新语法(提案)
class MyClass {
#secret = 'hidden';
@expose
get secret() {
return this.#secret;
}
}
function expose(target, context) {
context.access = 'public';
return function(value) {
this.#secret = value;
return value;
};
}
八、注意事项
- 装饰器是单向操作:一旦应用,无法撤销。
- 性能考虑:避免在装饰器中执行复杂计算。
- 兼容性:传统装饰器需 Babel 或 TypeScript 支持,新提案需现代浏览器或 Node.js。
九、总结
装饰器是 JavaScript 中强大的元编程工具,可用于:
- 代码复用(如日志、权限、缓存)。
- 分离关注点(如业务逻辑与横切关注点)。
- 简化 API(如通过装饰器替代复杂的配置)。