「刚刚学会了防抖,结果一到this指向我懵了!」 前端面试中,debounce 就是个常驻选手,自己用 OK,但一手写就开始原形毕露:
this怎么就丢了?- 定时器 ID 到底放哪儿?
- 闭包是怎么救场的?
不慌,这篇手把手拆解,写完这一个小 debounce,你对 闭包、this、定时器机制直接一网打尽,面试官都得点头说「行」!
debounce 是啥?别被人问住了
咱先别一上来就写代码,先问自己一个问题:
debounce 到底解决了什么问题?
想象几个场景
场景 1:搜索框输入
用户在搜索框里输入「ChatGPT」,如果每输入一个字母就发一次请求,后台服务器分分钟起飞 ,白白浪费带宽。
场景 2:滚动监听
监听 scroll 事件,页面滑动一下就疯狂触发 N 次,DOM 操作、接口调用…性能一夜回到解放前。
场景 3:窗口 resize
有些组件需要根据浏览器窗口大小自适应,resize 事件也是连环触发。
所以,debounce(防抖)就是「延迟执行」:
触发 n 次,只执行最后一次!!!
最简单的 debounce:先别管 this,跑起来再说
别怕,先从最简化版本开刀 👇
js
复制编辑
function debounce(fn, delay) {
let timer = null; // 用闭包保存上次的定时器 ID
return function() {
clearTimeout(timer); // 清除上一次的定时器
timer = setTimeout(fn, delay); // 重新设一个新的
};
}
解释一下:
- 外层
timer是闭包变量,永远跟着返回的函数。 - 每次调用时,先把前一个定时器砍掉(
clearTimeout)。 - 重新设一个新的
setTimeout。
这就能保证:
在 delay 时间内,如果有新的触发,就会把前面的清掉,只执行最后一次。
你以为结束了?this 让你泪流满面
别急,面试不会只让你写这么简单的。
要是有个对象,里面要用到 this 怎么办?咱看个实际点的例子
复制编辑
const obj = {
count: 0,
add() {
console.log('原始 count:', this.count);
this.count += 1;
console.log('更新后 count:', this.count);
}
};
const debouncedAdd = debounce(obj.add, 500);
debouncedAdd(); // ????
猜猜会发生什么?
this 丢了!
浏览器会报:
Cannot read property 'count' of undefined
为啥?
因为 setTimeout 里回调函数的 this 默认是 window(非严格模式),或者 undefined(严格模式)。
咱原来以为 this 是 obj,结果跑到别的地方去了。
🧙♂️ 所以,老司机怎么保住 this?
有三板斧:
bindcallapply
防抖里最常用的是 call 或 apply,搭配闭包把 this 保下来。
咱把刚刚的 debounce 升级一下
function debounce(fn, delay) {
return function(args) {
// 外层函数里的 this 就是调用者
const that = this;
clearTimeout(fn.id);
fn.id = setTimeout(function() {
fn.call(that, args);
}, delay);
};
}
这里多了几个关键点:
var that = this:把外层的this存到变量里。- 定时器回调里,
fn.call(that),把this重新指定回来。
现在谁调 inc,this 就是谁!
来看完整示例,this 正确绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Debounce this Demo</title>
</head>
<body>
<script>
function debounce(fn, delay) {
return function(args) {
var that = this;
clearTimeout(fn.id);
fn.id = setTimeout(function() {
fn.call(that, args);
}, delay);
};
}
let obj = {
count: 0,
inc: debounce(function(vel) {
console.log('count before:', this.count);
this.count += vel;
console.log('count after:', this.count);
}, 500)
};
obj.inc(2);
</script>
</body>
</html>
执行顺序:
obj.inc(2)调用时,this是obj。- 内层
that记录了obj。 - 定时器到点后,
fn.call(that),完美绑定回来。
定时器 ID 挂到 fn 上,好用但不优雅
有没有注意到?这里把 id 挂到了 fn 上:
fn.id = setTimeout(...)
为啥这么写?
JS 里函数是一等对象,想挂啥都行,临时挂个 id 省事。
但要注意:
如果多处用同一个
fn,可能互相覆盖。
严格生产里更推荐:
✅ 用闭包变量保存 id(局部,不怕冲突)。
✅ 或者用 Symbol 给函数打标签,不会被外界误用。
但面试手写,挂 fn.id 是老招,面试官能看懂也懒得挑刺。
如果要传多个参数怎么办?
还记得前面示例是 args 吗?那是单参数的,多个参数要用 ... 和 apply:
function debounce(fn, delay) {
return function(...args) {
var that = this;
clearTimeout(fn.id);
fn.id = setTimeout(function() {
fn.apply(that, args);
}, delay);
};
}
apply 可以把数组展开传进去,完美支持多参数场景。
真·生产级防抖还有哪些花活?
手写只是入门,真要落地还得会玩进阶版:
✅ 立即执行(leading edge)
默认防抖是“尾触发”,但有些需求要“先触发一次”。
典型场景:输入框实时搜索,先返回一个缓存结果。
✅ 最大等待时间(maxWait)
有些需求不能等太久,一定时间内至少执行一次。
lodash 的 debounce 就支持这个。
✅ 和 throttle(节流)组合
有些场景先防抖后节流,比如滚动监听。
##当然可以!这是针对你关于 this 丢失与绑定问题的文章小结版,写得简洁明了,适合直接放在文章里帮助读者抓重点:
小结(重难点!!!):this 丢失的真相与正确绑定姿势
在防抖函数的实现中,this 的指向至关重要。常见坑点在于:
- 千万别在
debounce外层函数里保存this!
因为调用debounce本身时,this通常是全局对象(window)或undefined,保存的就不是我们想要的上下文。 - 真正正确的做法,是在防抖返回的那个函数内部捕获
this。
这是因为调用防抖函数(如obj.inc())时,this会正确指向调用者obj,这里保存的this才是有效的上下文。 - 定时器回调函数中,
this又会丢失(指向全局或 undefined) ,必须用call或apply显式绑定回正确的this。
总结一句话:
“
this的指向由调用时决定,保存this要靠闭包捕获调用防抖函数时的上下文,再通过call/apply绑定到延迟执行的回调里。”
掌握这三步,this 丢失问题迎刃而解,防抖写起来更稳妥,面试官问也不用慌!
今天这锅,咱端稳了!
- 防抖(debounce)的核心场景和作用
this在回调里怎么保住不丢call/apply的使用姿势- 定时器 ID 怎么挂、怎么管
- 多参数怎么传递
- 面试手写避坑指南
彩蛋互动
你平时是自己写防抖,还是直接 lodash 一把梭?
或者面试真被问过 this 和闭包吗?
评论区告诉我,你最想看下次我手写哪个工具函数?
结尾
如果这篇对你有帮助,麻烦点个小小的 ❤️ 和 收藏,
别被防抖坑了,让我们一起把面试官抖没脾气!