Skip to main content

装饰器

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...');
}
}

四、装饰器执行顺序

  1. 参数装饰器方法装饰器属性装饰器类装饰器
  2. 同一类型的装饰器,从下到上(或从内到外)执行。

示例

@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;
};
}

八、注意事项

  1. 装饰器是单向操作:一旦应用,无法撤销。
  2. 性能考虑:避免在装饰器中执行复杂计算。
  3. 兼容性:传统装饰器需 Babel 或 TypeScript 支持,新提案需现代浏览器或 Node.js。

九、总结

装饰器是 JavaScript 中强大的元编程工具,可用于:

  • 代码复用(如日志、权限、缓存)。
  • 分离关注点(如业务逻辑与横切关注点)。
  • 简化 API(如通过装饰器替代复杂的配置)。