类装饰器,作用的是整个类。但如果想要给类里面的某个方法增加功能,而不影响其他的方法,此时我们就可以使用 “方法装饰器” 来实现。
TypeScript 方法装饰器是什么?
在 TypeScript 中,方法装饰器放在 “类成员方法” 上方的。当类成员方法被定义时,TypeScript 会自动调用装饰器函数,并传入 3 个关键的参数。
target:对于静态成员来说,是类的构造函数。对于实例成员来说,是类的原型对象 。propertyKey:被装饰的方法的名字(字符串)。descriptor:该方法的属性描述符(这是实现功能拦截的核心)。
示例 1:方法装饰器的内部信息
// 定义方法装饰器
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 1. 先把原来那个纯粹的 getUsers 方法保存下来
const originalMethod = descriptor.value;
// 2. 强行修改(重写)原来的方法
// 修正:严格模式下,必须显式声明 this: any,防止 this 隐式报错
descriptor.value = function (this: any, ...args: any[]) {
// 在原方法执行前,搞点小动作(比如记录日志)
console.log("[系统]: 准备拦截并执行方法 ->", propertyKey);
// 3. 真正执行原来的方法,记得使用 apply 把 this 绑定回去,防止 this 丢失
const result = originalMethod.apply(this, args);
// 在原方法执行后,再搞点小动作
console.log("[系统]: 方法执行完毕 ->", propertyKey);
// 4. 返回原方法本该返回的结果
return result;
};
}运行结果如下。
[系统]: 准备拦截并执行方法 -> getUsers
正在查询用户列表...
[系统]: 方法执行完毕 -> getUsers分析:
在这个例子中,装饰器函数 LogMethod() 依然会在类加载时执行,但它这次干的事情是:把底层的 getUsers() 方法偷偷替换成了一个 “包裹了日志逻辑的新函数”。
当我们在最后一行调用 service.getUsers() 时,实际执行的是我们在装饰器里写的 descriptor.value = function(...) { ... } 这段逻辑。这种不修改原业务代码,却能在外面包裹一层通用逻辑的手法,正是面向切面编程(AOP)的核心精髓。
TypeScript 方法装饰器的应用
接下来,我们通过 3 个例子来介绍一下 TypeScript 方法装饰器是如何使用的。
1. 实现一个通用的日志拦截器
在企业级开发中,我们经常需要记录某个重要操作的执行耗时。如果不使用装饰器,我们就得到每个函数里手动改,这样的代码重复性是非常高的。而有了方法装饰器之后,我们就可以优雅地实现一个通用的日志拦截器。
示例 2:实现日志拦截器
function MeasureTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 1. 先把原本的函数逻辑存起来
const originalMethod = descriptor.value;
// 2. 重写原本的函数
descriptor.value = function (this: any, ...args: any[]) {
console.log(`--- [${propertyKey}] 开始执行 ---`);
const start = Date.now();
// 3. 执行原本的函数逻辑(注意:这里必须使用 apply 绑定 this)
const result = originalMethod.apply(this, args);
const end = Date.now();
console.log(`--- [${propertyKey}] 执行结束,耗时:${end - start}ms ---`);
return result;
};
}运行结果如下。
--- [calculateHeavyTask] 开始执行 ---
业务逻辑执行中...
--- [calculateHeavyTask] 执行结束,耗时:29ms ---分析:
这段代码展示了方法装饰器的 “标准姿势”:
- 保存原函数:通过 descriptor.value 拿到原本的方法。
- 函数包装:给 descriptor.value 重新赋一个新函数。在新函数里,我们可以先执行一些逻辑(如记录开始时间),然后再调用 originalMethod.apply(this, args) 触发原有的业务代码。
- 无感注入:对于业务人员来说,只需要在方法上贴个 @MeasureTime,功能就自动实现了。
2. 实现一个全局异常捕获拦截器
在处理网络请求或文件读写时,如果每一个地方都写 try-catch,代码会非常冗余。我们可以封装一个装饰器,让它自动帮我们兜底报错。
示例 3:自动异常处理
function CatchError(msg: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (this: any, ...args: any[]) {
try {
return originalMethod.apply(this, args);
} catch (error) {
console.error("[全局捕获]: ", msg);
console.error("错误详情:", error);
}
};
};
}运行结果如下。
[全局捕获]: 获取配置接口发生崩溃
错误详情: Error: 404分析:
在这个例子中,我们结合了 “装饰器工厂” 的思想,让装饰器可以接收自定义的错误提示。这种写法在封装底层 SDK 或 UI 组件库时经常会用到,因为它能极大地提升程序的健壮性。
3. 实现异步方法的耗时统计拦截器(进阶必考)
前面我们写的 MeasureTime 只能统计普通的同步代码。但在真实的业务中,比如请求接口或查询数据库,通常都是异步操作(async/await)。
如果一个函数返回的是 Promise,我们的装饰器就需要判断返回值类型,并在 Promise 结束后再统计耗时。这也是很多初学者容易踩坑的地方。
示例 4:支持异步方法的终极耗时统计
function MeasureAsyncTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
// 针对异步函数的包裹,同样需要显式标注 this: any
descriptor.value = async function (this: any, ...args: any[]) {
console.log(`--- [${propertyKey}] 异步任务开始 ---`);
const start = Date.now();
// 核心区别:我们必须使用 await 来等待原方法的异步逻辑执行完毕
const result = await originalMethod.apply(this, args);
const end = Date.now();
console.log(`--- [${propertyKey}] 异步任务结束,耗时:${end - start}ms ---`);
return result;
};
}运行结果如下。
--- [fetchData] 异步任务开始 ---
--- [fetchData] 异步任务结束,耗时:2001ms ---
获取到的数据:绿叶网教程分析:
在这个终极版的拦截器中,我们将 descriptor.value 包装成了一个 async function,并在内部使用 await originalMethod.apply(...)。
这样一来,装饰器就会老老实实地等待网络请求或数据库查询彻底结束,然后再执行后置的日志打印逻辑。这就是阅读 Axios 或 NestJS 拦截器源码时必须掌握的基础知识。
