JavaScript Proxy 的 handler.apply() :拦截函数调用
在 JavaScript 的世界里,Proxy 是一个强大的特性,它允许我们创建一个对象的代理,从而拦截并重新定义该对象的基本操作。今天,我们来深入聊聊其中一个很实用的方法:handler.apply()。
这个方法专门用来拦截对函数的调用。无论是直接调用函数,还是通过 apply() 或 call() 方法间接调用,只要目标对象是一个函数,apply 就能介入。
一、handler.apply() 到底是什么?
简单理解,handler.apply() 就像一个守门人。当你试图调用一个被代理的函数时,这个守门人会先“检查”你的请求,然后决定如何响应,甚至可以选择修改调用时的参数或返回值。
语法结构:
let proxy = new Proxy(targetFunction, {
apply: function(target, thisArg, argumentsList) {
// 自定义逻辑
return target.apply(thisArg, argumentsList);
}
});
参数解析:
-
target:被代理的原始函数对象,也就是我们的目标。 -
thisArg:调用函数时,this关键字所指向的对象。这个参数在代理内部非常关键,因为它决定了函数的执行上下文。 -
argumentsList:调用函数时传入的参数列表,是一个类数组对象。
返回值: 我们可以返回任何值,这个值会作为函数调用的结果返回给调用者。
二、浏览器支持情况
| Chrome | Edge | Firefox | Opera |
|---|---|---|---|
| 49 | 12 | 18 | 36 |
在2026年的今天,几乎所有现在浏览器都支持 Proxy 和 handler.apply(),可以放心在生产环境中使用。
三、核心示例:从基础到进阶
让我们通过一些实际的例子,看看 handler.apply() 如何工作。
示例 1:拦截函数调用,添加日志记录
这是 apply 最常见的用途之一——在不修改原函数的情况下,为函数调用添加日志功能。下面的例子中,我们创建了一个计算两数之和的函数,并通过代理为它添加了参数验证和调用日志。
<script>
// 原始函数:计算两个数的和
let sumFunction = function(a, b) {
document.writeln(`正在计算: ${a} + ${b}<br>`);
return a + b;
};
// 创建代理,拦截 apply 操作
let proxySum = new Proxy(sumFunction, {
apply: function(target, thisArg, argumentsList) {
// 先记录日志
document.writeln(`【代理日志】函数被调用了<br>`);
document.writeln(`【代理日志】传入参数: ${argumentsList[0]}, ${argumentsList[1]}<br>`);
// 调用原始函数,并获取结果
let result = target.apply(thisArg, argumentsList);
document.writeln(`【代理日志】计算结果: ${result}<br>`);
return result;
}
});
// 通过代理调用函数
let sumResult = proxySum(15, 28);
document.writeln(`最终结果: ${sumResult}<br>`);
</script>
输出:
【代理日志】函数被调用了
【代理日志】传入参数: 15, 28
正在计算: 15 + 28
【代理日志】计算结果: 43
最终结果: 43
个人见解: 这种模式在开发中非常实用。比如在代码号学习编程时,我们常常需要调试某个函数被调用的频率和参数,但又不想直接修改原函数代码。通过 apply ,我们可以轻松实现无侵入式的日志记录和性能监控。
示例 2:拦截不同的调用方式(直接调用、apply、call)
handler.apply() 不仅能拦截直接调用,还能拦截 apply() 和 call() 方法。下面这个例子展示了这一点:
<script>
let showMessage = function(msg) {
document.writeln(`原始函数执行: ${msg}<br>`);
return msg;
};
let messageProxy = new Proxy(showMessage, {
apply: function(target, thisArg, argumentsList) {
document.writeln(`【代理拦截】捕获到调用<br>`);
document.writeln(`【代理拦截】thisArg: ${thisArg}<br>`);
document.writeln(`【代理拦截】参数列表: ${argumentsList}<br>`);
let result = target.apply(thisArg, argumentsList);
document.writeln(`【代理拦截】返回结果: ${result}<br><br>`);
return `【代理包装】${result}`;
}
});
// 三种不同的调用方式
document.writeln(`--- 方式1: 直接调用 ---<br>`);
messageProxy("直接调用测试");
document.writeln(`--- 方式2: apply调用 ---<br>`);
messageProxy.apply(null, ["apply调用测试"]);
document.writeln(`--- 方式3: call调用 ---<br>`);
messageProxy.call(null, "call调用测试");
</script>
输出:
--- 方式1: 直接调用 ---
【代理拦截】捕获到调用
【代理拦截】thisArg: undefined
【代理拦截】参数列表: 直接调用测试
原始函数执行: 直接调用测试
【代理拦截】返回结果: 直接调用测试
--- 方式2: apply调用 ---
【代理拦截】捕获到调用
【代理拦截】thisArg: null
【代理拦截】参数列表: apply调用测试
原始函数执行: apply调用测试
【代理拦截】返回结果: apply调用测试
--- 方式3: call调用 ---
【代理拦截】捕获到调用
【代理拦截】thisArg: null
【代理拦截】参数列表: call调用测试
原始函数执行: call调用测试
【代理拦截】返回结果: call调用测试
示例 3:修改函数返回值,实现计算增强
有时候,我们希望在原有函数的基础上,对返回值进行一些处理。这个例子展示了如何通过代理,实现返回值的增强:
<script>
// 原始函数:计算单价和数量的乘积
let calculatePrice = function(unitPrice, quantity) {
return unitPrice * quantity;
};
// 创建代理,拦截返回值并增加税费计算
let priceProxy = new Proxy(calculatePrice, {
apply: function(target, thisArg, argumentsList) {
let unitPrice = argumentsList[0];
let quantity = argumentsList[1];
// 计算原价
let subtotal = target.apply(thisArg, argumentsList);
// 假设税费率为 10%
let tax = subtotal * 0.1;
let total = subtotal + tax;
document.writeln(`商品单价: ${unitPrice} 元<br>`);
document.writeln(`购买数量: ${quantity}<br>`);
document.writeln(`小计: ${subtotal} 元<br>`);
document.writeln(`税费(10%): ${tax} 元<br>`);
document.writeln(`总计: ${total} 元<br>`);
return total;
}
});
let finalPrice = priceProxy(99, 3);
document.writeln(`最终支付金额: ${finalPrice} 元`);
</script>
输出:
商品单价: 99 元
购买数量: 3
小计: 297 元
税费(10%): 29.7 元
总计: 326.7 元
最终支付金额: 326.7 元
四、核心要点与实际应用场景
本节课程知识要点:
-
拦截范围广泛:
handler.apply()能够拦截所有形式的函数调用,包括直接调用、apply()、call()以及通过扩展运算符进行的调用。 -
thisArg 的处理:在代理内部调用
target.apply(thisArg, argumentsList)时,务必传递正确的thisArg,否则原函数内部的this指向可能会出错。如果不确定,保留原始的thisArg是最稳妥的做法。 -
返回值的灵活性:
apply的返回值可以是任意类型。你可以修改原函数的返回值,也可以不调用原函数,直接返回自定义的值。这种灵活性为很多高级模式提供了可能。 -
与 Reflect 的配合:在开发中,推荐使用
Reflect.apply(target, thisArg, argumentsList)来替代target.apply(thisArg, argumentsList),因为Reflect提供了更统一的 API 和更好的错误处理机制。
个人建议:
在项目中,handler.apply() 最常见的应用场景包括:
-
性能监控:统计函数被调用的次数和执行时间
-
参数验证:在调用前检查参数是否符合预期
-
结果缓存:对于计算密集型的函数,可以实现结果缓存,避免重复计算
-
权限控制:根据当前用户权限决定是否允许调用某个函数
为什么选择用 Proxy 的 apply 而不是直接修改原函数?因为直接修改原函数会破坏代码的单一职责原则,而且当需要多种增强功能时,代理模式提供了更好的解耦和组合能力。通过代理,我们可以把横切关注点(如日志、缓存、权限)与核心业务逻辑分离,让代码更易于维护和测试。