标准答案
- 防抖:每次触发先清除旧定时器,再设置新定时器;只有在一段时间内不再触发,才执行原函数。适合输入搜索、resize 重新计算、表单校验等最终态场景。
- 节流:在持续触发时控制执行频率,保证一段时间内最多执行一次或按固定节奏执行。适合滚动监听、拖拽、埋点上报等持续反馈场景。
- 保留 this 和参数:包装函数要用普通函数接收调用方的 this 和 args,再通过 fn.apply(this, args) 调用原函数。不要在需要动态 this 的场景里把包装器写成箭头函数。
- 完整实现:防抖通常需要支持立即执行和 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 不符合调用方预期。
- 忘记透传参数。
- 节流实现只有头部执行,没有尾部执行,导致最后一次输入丢失。