文章目录

JavaScript防抖和节流为什么用闭包实现更优雅

发布于 2026-04-27 18:30:59 · 浏览 5 次 · 评论 0 条

在处理高频事件(如窗口大小调整、滚动、输入框输入)时,浏览器会被大量重复的任务阻塞,导致页面卡顿。防抖和节流是解决这一问题的两种核心策略。大多数初级开发者会使用全局变量来存储计时器状态,但这不仅污染全局命名空间,还导致同一个函数无法在页面上复用。

使用闭包可以将计时器状态“私有化”,让每个函数调用都拥有独立的作用域。以下是具体实现步骤。


第一步:理解闭包解决的核心痛点

如果不使用闭包,你必须在全局作用域声明变量。假设你有两个输入框都需要防抖处理,全局变量 timer 会被第二个输入框的逻辑覆盖或干扰,导致第一个输入框的功能失效。

需要明确的目标

  1. 隐藏状态:变量 timerlastTime 不应暴露给外部。
  2. 独立作用域:每次调用封装函数,都生成一份全新的独立状态。

第二步:实现防抖函数

防抖的核心逻辑是:“尽管你触发了多次,我只在最后一次触发后的等待时间里没有新的触发,才执行。”

使用闭包重构防抖逻辑

  1. 定义 一个名为 debounce 的函数,接收两个参数:fn(要执行的函数)和 delay(延迟时间)。
  2. 声明 一个局部变量 timer。这个变量虽然在外部函数内,但因为内部返回的函数引用了它,所以它会常驻内存,不会被销毁。
  3. 返回 一个匿名函数作为事件处理函数。
  4. 在返回的函数内部,首先 执行 clearTimeout(timer),清除上一次未执行的计时器。
  5. 重新赋值 timer,使用 setTimeout 并将延迟时间设为 delay
  6. 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)

第三步:实现节流函数

节流的核心逻辑是:“尽管你触发了多次,我每隔固定的时间段最多只执行一次。”

使用闭包重构节流逻辑

  1. 定义 一个名为 throttle 的函数,接收 fndelay
  2. 声明 两个局部变量:lastTime(上次执行的时间戳)和 timer
  3. 返回 一个匿名函数。
  4. 返回的函数内部,获取 当前时间戳 now
  5. 判断 如果 now - lastTime > delay,说明冷却时间已过。
  6. 如果冷却结束立即调用 fn.apply(this, arguments),并 更新 lastTime = now
  7. 处理边缘情况(可选但推荐),如果在冷却期内最后一次触发被遗忘了,可以使用 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;
        }
    };
}

第四步:在实际代码中应用

现在,你可以安全地在同一个页面的不同元素上复用这些函数,而互不干扰。

  1. 获取 DOM 元素,例如一个输入框和一个滚动容器。
  2. 调用 debouncethrottle 生成专属的处理函数。
  3. 绑定 事件监听器。
// 假设的 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);

核心区别对比

下表总结了全局变量实现与闭包实现的区别,体现了闭包的优雅之处。

特性 全局变量实现 闭包实现
变量作用域 全局共享,极易冲突 函数内部私有,互不干扰
代码复用性 差,无法在同一页面对不同元素使用不同配置 强,每次调用生成新的独立环境
维护成本 高,需要手动管理变量名和清理 低,变量随函数引用自动管理
参数传递 麻烦,难以保存 thisarguments 简单,利用 apply 完美还原执行上下文

通过闭包,我们实际上是在函数外部创建了一个“私有钱包”(变量 timerlastTime),只有函数自己能存取这个钱包里的钱(状态),外部代码既偷不走也乱改不了,这就是它更优雅的本质原因。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文