JavaScript装饰器结合Reflect Metadata可实现AOP,通过类、方法、属性和参数装饰器解耦横切逻辑;利用Reflect.defineMetadata存储元数据,如权限角色,并在执行时动态拦截方法进行校验,实现日志、权限控制等功能,提升代码复用与可维护性。

JavaScript 装饰器是一种强大的元编程特性,允许我们在类、方法、属性或参数定义时附加逻辑,改变其行为。虽然目前仍处于提案阶段(Stage 3),但通过 Babel 或 TypeScript 已可广泛使用。结合元数据反射(Reflect Metadata),装饰器能实现面向切面编程(AOP),将横切关注点如日志、权限校验、性能监控等与业务逻辑解耦。
装饰器基础:语法与类型
装饰器本质是一个函数,接收目标对象、属性名和描述符作为参数,在运行时被调用。根据应用位置不同,分为:
- 类装饰器:作用于类构造函数,可用于修改类行为或替换类定义
- 方法装饰器:拦截方法调用,常用于控制执行逻辑
- 属性装饰器:标记或初始化属性
- 参数装饰器:为参数添加元数据,常用于依赖注入
示例:一个简单的日志方法装饰器
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling "${propertyKey}" with`, args);
return originalMethod.apply(this, args);
};
}
元数据反射:使用 Reflect Metadata
要实现更复杂的 AOP 场景,需借助元数据系统存储额外信息。TypeScript 配合 reflect-metadata 库可实现这一能力。
立即学习“Java免费学习笔记(深入)”;
启用步骤:
- 安装包:
npm install reflect-metadata --save - TypeScript 配置开启
emitDecoratorMetadata: true - 入口文件导入:
import 'reflect-metadata'
通过 Reflect.defineMetadata 和 Reflect.getMetadata 存取元数据,例如标记某个方法需要权限校验:
return function (target: any, propertyKey: string) {
Reflect.defineMetadata('role', role, target, propertyKey);
};
}
面向切面编程:构建可复用切面
AOP 的核心是将横切逻辑集中管理。利用装饰器与元数据,可在不侵入业务代码的前提下织入切面逻辑。
实现一个权限检查切面:
function Secure(target: any, propertyKey: string, descriptor: PropertyDescriptor) {const requiredRole = Reflect.getMetadata('role', target, propertyKey);
if (!requiredRole) return;
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const userRole = this.userRole; // 假设实例上有用户角色
if (userRole !== requiredRole) {
throw new Error('Access denied');
}
return originalMethod.apply(this, args);
};
}
使用方式:
class UserService {userRole = 'admin';
@RequireRole('admin')
@Secure
deleteUser(id: number) {
console.log('User deleted:', id);
}
}
实际应用场景与注意事项
常见用途包括:
- 自动日志记录与错误追踪
- 输入验证与参数转换
- 缓存结果提升性能
- 事务管理与资源清理
注意点:
- 装饰器在类定义时执行,而非实例化时
- 避免在装饰器中引用未初始化的变量
- 谨慎修改原方法签名,可能影响类型安全
- 生产环境建议配合静态分析工具确保稳定性
基本上就这些。装饰器+元数据为 JavaScript 提供了接近框架级的能力,合理使用能让代码更清晰、职责更分明。虽然标准尚未最终落地,但在 TypeScript 生态中已是成熟实践。不复杂但容易忽略的是元数据的键冲突问题,建议使用 Symbol 作为 key 来避免命名污染。











