标准答案

  1. 防抖:每次触发先清除旧定时器,再设置新定时器;只有在一段时间内不再触发,才执行原函数。适合输入搜索、resize 重新计算、表单校验等最终态场景。
  2. 节流:在持续触发时控制执行频率,保证一段时间内最多执行一次或按固定节奏执行。适合滚动监听、拖拽、埋点上报等持续反馈场景。
  3. 保留 this 和参数:包装函数要用普通函数接收调用方的 this 和 args,再通过 fn.apply(this, args) 调用原函数。不要在需要动态 this 的场景里把包装器写成箭头函数。
  4. 完整实现:防抖通常需要支持立即执行和 cancel;节流通常需要处理 leading、trailing,避免最后一次触发被丢掉。取消执行就是清理定时器并重置内部状态。

题目解析

这里经常需要结合手写实现理解。不要只给定义,也要说明如何保留 this、参数和如何取消。

防抖的核心是“等用户停下来”,节流的核心是“持续动作中按频率执行”。先从场景判断策略,再决定是否需要立即执行、尾部执行和取消能力。

this 和参数是手写题里最容易漏的基础边界。包装函数不能假设原函数没有上下文,也不能丢掉事件对象、输入值等参数。

真实项目可以优先用成熟工具函数,但要知道它们的 leading、trailing、cancel 等选项分别解决什么问题,否则很容易在搜索、滚动和提交场景里选错配置。

代码示例

下面示例包含可取消的防抖,以及带尾部执行的节流。

JavaScript
function debounce(fn, wait, immediate = false) {
  let timer = null

  function debounced(...args) {
    const shouldCallNow = immediate && !timer

    clearTimeout(timer)
    timer = setTimeout(() => {
      timer = null
      if (!immediate) {
        fn.apply(this, args)
      }
    }, wait)

    if (shouldCallNow) {
      fn.apply(this, args)
    }
  }

  debounced.cancel = () => {
    clearTimeout(timer)
    timer = null
  }

  return debounced
}

function throttle(fn, wait) {
  let lastTime = 0
  let timer = null

  return function throttled(...args) {
    const now = Date.now()
    const remaining = wait - (now - lastTime)

    if (remaining <= 0) {
      clearTimeout(timer)
      timer = null
      lastTime = now
      fn.apply(this, args)
      return
    }

    if (!timer) {
      timer = setTimeout(() => {
        lastTime = Date.now()
        timer = null
        fn.apply(this, args)
      }, remaining)
    }
  }
}

常见误区

  • 用箭头函数写包装器导致 this 不符合调用方预期。
  • 忘记透传参数。
  • 节流实现只有头部执行,没有尾部执行,导致最后一次输入丢失。

作者信息