TypeScript 方法装饰器

类装饰器,作用的是整个类。但如果想要给类里面的某个方法增加功能,而不影响其他的方法,此时我们就可以使用 “方法装饰器” 来实现。

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 拦截器源码时必须掌握的基础知识。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号