一段简单的防抖函数
const btn = document.querySelector('#btn')
// 设置初始化timer
function debounce(fn,delay) {
let timer = null
return function(...args){
// 如果timer有值,清除timer
clearTimeout(timer)
// 将计时器id给timer
timer = setTimeout(() => {
console.log(this); // 有this
let that = this
fn.apply(that,args)
}, delay)
}
}
// 每次点击,调用debounce
btn.addEventListener('click',debounce(function(e) {
console.log(e);
console.log(this);//有this
},1000))
1.闭包在初始化时的调用 timer
btn.addEventListener('click',debounce(fn,1000))
此时初始化执行一次debounce(),后续点击事件触发时,执行的是debounce()所返回的函数,所以timer只会在页面加载时初始化一次,后续重新触发debounce()返回的函数时不会重新初始化,这就使得每一次事件都能保留timer的值
AI补充:"debounce() 只在初始化时执行一次,返回的函数通过闭包保留了 timer,因此多次触发时能共享同一个 timer。"
更准确的说法是:"timer 在 debounce 函数被调用时初始化(只执行一次),但由于闭包的特性,返回的函数能持续访问同一个 timer 变量"
2.this的调用
btn.addEventListener('click',debounce(()=>{
console.log(this)//此时是window
},1000))
为什么此时是window呢?因为箭头函数的this由定义时的上下文决定,所以当箭头函数在此时定义时,相当于
const fn = ()=>{console.log(this)}
btn.addEventListener('click',debounce(fn,1000))
这样就能看出来虽然fn被传入了debounce,但是在定义时已经决定了this为window,就算后续fn.apply(that,args) ,也改变不了this的值,所以只能把传入的函数写成普通函数,才能在后续绑定正确的this
AI补充:"箭头函数的 this 是词法作用域(定义时决定),而普通函数的 this 是动态作用域(调用时决定)。因此:
- 箭头函数:
this固定为定义时的上下文(如window) - 普通函数:
this由调用方式决定(如fn.call(ctx)可绑定this)"
可以补充:"箭头函数的 this 是词法作用域的,它在定义时捕获外层函数的 this。如果箭头函数在全局作用域定义,则 this 始终为 window,即使通过 apply/call 也无法改变"
3.debonce函数内的fn函数绑定this的问题
btn.addEventListener('click',debounce(funtion(){
console.log(this) // 此时this也为window
},1000))
// 如果timer有值,清除timer
clearTimeout(timer)
// 将计时器id给timer
timer = setTimeout(() => {
console.log(this); // 有this
let that = this
fn() //在全局调用,此时传入的this为window
}, delay)
如果fn不显式绑定this,这时候在外层输出的this也为window,因为此时在debounce函数内调用了fn,因为这时候调用的fn(),并没有通过谁来调用,而是在script内凭空调用的fn,所以fn指向的是全局window
AI补充:"直接调用 fn() 等价于 fn.call(undefined),非严格模式下 this 会指向 window。
必须通过 fn.apply(that, args) 显式绑定 this。"
明确说明根本原因:"直接调用 fn() 时,相当于在全局上下文执行函数(非严格模式 this 为 window,严格模式为 undefined)。必须通过 fn.apply(this) 显式绑定事件监听器的 this(即 btn)"
4.事件对象的传入传出
btn.addEventListener('click',debounce(function(e) {
console.log(e);//输出事件对象
console.log(this);//有this
},1000))
return function(...args){
// 如果timer有值,清除timer
clearTimeout(timer)
// 将计时器id给timer
timer = setTimeout(() => {
console.log(this); // 有this
let that = this
fn.apply(that,args)
}, delay)
}
由于在事件函数内触发时调用的是debounce返回的函数,所以事件对象也被addEventListener方法给注入进了返回的函数,这时候我们就可以用..args接收,所接收的args则为事件对象所处的数组,再将args通过apply方法传递给我们自己写的fn回调函数,这样就可以在debounce函数外层fn的参数里,得到我们想要的事件对象e
AI补充:"...args 接收的是事件回调的所有参数(如 [event]),通过 apply 传递给 fn,因此 fn(e) 能拿到事件对象。"
更精确的描述:"...args 会接收事件监听器传入的所有参数(第一个参数通常是事件对象 e)。通过 fn.apply(this, args),这些参数会被原样传递给回调函数 fn"
最后AI还补充了clearTimeout安全性、内存泄露问题、立即执行选项