在处理高频事件(如窗口大小调整、滚动、输入框输入)时,浏览器会被大量重复的任务阻塞,导致页面卡顿。防抖和节流是解决这一问题的两种核心策略。大多数初级开发者会使用全局变量来存储计时器状态,但这不仅污染全局命名空间,还导致同一个函数无法在页面上复用。
使用闭包可以将计时器状态“私有化”,让每个函数调用都拥有独立的作用域。以下是具体实现步骤。
第一步:理解闭包解决的核心痛点
如果不使用闭包,你必须在全局作用域声明变量。假设你有两个输入框都需要防抖处理,全局变量 timer 会被第二个输入框的逻辑覆盖或干扰,导致第一个输入框的功能失效。
需要明确的目标:
- 隐藏状态:变量
timer或lastTime不应暴露给外部。 - 独立作用域:每次调用封装函数,都生成一份全新的独立状态。
第二步:实现防抖函数
防抖的核心逻辑是:“尽管你触发了多次,我只在最后一次触发后的等待时间里没有新的触发,才执行。”
使用闭包重构防抖逻辑:
- 定义 一个名为
debounce的函数,接收两个参数:fn(要执行的函数)和delay(延迟时间)。 - 声明 一个局部变量
timer。这个变量虽然在外部函数内,但因为内部返回的函数引用了它,所以它会常驻内存,不会被销毁。 - 返回 一个匿名函数作为事件处理函数。
- 在返回的函数内部,首先 执行
clearTimeout(timer),清除上一次未执行的计时器。 - 重新赋值
timer,使用setTimeout并将延迟时间设为delay。 - 在
setTimeout的回调中,调用fn.apply(this, arguments),确保this指向和参数正确传递。
function debounce(fn, delay) {
// 闭包变量,对外部不可见
let timer = null;
return function() {
// 保存当前的 this 和 arguments,防止 setTimeout 改变指向
let context = this;
let args = arguments;
// 清除上一次的计时器
if (timer) {
clearTimeout(timer);
}
// 开启新的计时器
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
};
}
为了更直观地理解闭包如何管理计时器,请看以下时序流程:
sequenceDiagram
participant User as 用户
participant Outer as debounce外层
participant Inner as 返回的闭包函数
participant Timer as 计时器
User->>Inner: 触发事件(输入A)
Inner->>Inner: 检查 timer (null)
Inner->>Timer: "创建: 延迟500ms"
Note over User,Timer: 间隔200ms
User->>Inner: 触发事件(输入B)
Inner->>Timer: "清除: 上次计时器"
Inner->>Timer: "创建: 延迟500ms"
Note over User,Timer: 间隔500ms (无新输入)
Timer->>Inner: 时间到
Inner->>Outer: 执行 fn (输入B)
第三步:实现节流函数
节流的核心逻辑是:“尽管你触发了多次,我每隔固定的时间段最多只执行一次。”
使用闭包重构节流逻辑:
- 定义 一个名为
throttle的函数,接收fn和delay。 - 声明 两个局部变量:
lastTime(上次执行的时间戳)和timer。 - 返回 一个匿名函数。
- 在 返回的函数内部,获取 当前时间戳
now。 - 判断 如果
now - lastTime > delay,说明冷却时间已过。 - 如果冷却结束,立即调用
fn.apply(this, arguments),并 更新lastTime = now。 - 处理边缘情况(可选但推荐),如果在冷却期内最后一次触发被遗忘了,可以使用
timer在节流结束后执行一次,这里展示最基础的时间戳版。
function throttle(fn, delay) {
// 上一次执行的时间戳
let lastTime = 0;
return function() {
let context = this;
let args = arguments;
let now = Date.now();
// 如果距离上次执行的时间超过了 delay
if (now - lastTime > delay) {
fn.apply(context, args);
// 更新上次执行时间
lastTime = now;
}
};
}
第四步:在实际代码中应用
现在,你可以安全地在同一个页面的不同元素上复用这些函数,而互不干扰。
- 获取 DOM 元素,例如一个输入框和一个滚动容器。
- 调用
debounce或throttle生成专属的处理函数。 - 绑定 事件监听器。
// 假设的 DOM 元素
const input1 = document.getElementById('search-input-1');
const input2 = document.getElementById('search-input-2');
// 生成两个独立的防抖处理函数,互不干扰
const handleInput1 = debounce(function(e) {
console.log('发送请求 1:', e.target.value);
}, 500);
const handleInput2 = debounce(function(e) {
console.log('发送请求 2:', e.target.value);
}, 1000); // 甚至可以使用不同的延迟时间
// 绑定事件
input1.addEventListener('input', handleInput1);
input2.addEventListener('input', handleInput2);
核心区别对比
下表总结了全局变量实现与闭包实现的区别,体现了闭包的优雅之处。
| 特性 | 全局变量实现 | 闭包实现 |
|---|---|---|
| 变量作用域 | 全局共享,极易冲突 | 函数内部私有,互不干扰 |
| 代码复用性 | 差,无法在同一页面对不同元素使用不同配置 | 强,每次调用生成新的独立环境 |
| 维护成本 | 高,需要手动管理变量名和清理 | 低,变量随函数引用自动管理 |
| 参数传递 | 麻烦,难以保存 this 和 arguments |
简单,利用 apply 完美还原执行上下文 |
通过闭包,我们实际上是在函数外部创建了一个“私有钱包”(变量 timer 或 lastTime),只有函数自己能存取这个钱包里的钱(状态),外部代码既偷不走也乱改不了,这就是它更优雅的本质原因。

暂无评论,快来抢沙发吧!