防抖函数中的this指向和闭包和默认参数

327 阅读7分钟

前言:

一、什么是防抖?

在JavaScript中,防抖是一种编程技术,主要用于限制某个函数在短时间内被频繁调用的次数。当某个事件被频繁触发时(如窗口的resize事件、输入框的input事件等),如果每次触发都执行对应的处理函数,可能会导致浏览器性能问题或不必要的计算。通过防抖,我们可以确保在处理函数被调用之前,必须等待一段时间,以确保在这段时间内事件没有被再次触发。

二、防抖函数的作用和好处:

  1. 性能优化:当事件被频繁触发时(例如,用户持续滚动页面或快速输入文本),如果不使用防抖函数,对应的处理函数也会被频繁执行,这可能会导致浏览器性能下降,页面卡顿或响应不及时。使用防抖函数可以确保处理函数只在事件停止触发后的一段时间内执行一次,从而优化性能。
  2. 减少不必要的计算:在某些情况下,处理函数可能需要执行复杂的计算或调用API。如果这些操作被频繁触发,不仅会增加服务器的负担,还可能浪费用户的时间和带宽。防抖函数可以确保这些操作只在需要时执行,从而避免不必要的计算和开销。
  3. 改善用户体验:在某些情况下,防抖函数可以改善用户体验。例如,在搜索框中输入文本时,通常会在每次输入时发送请求以获取搜索建议。然而,如果用户输入速度很快,那么可能会发送大量的请求,这不仅会增加服务器的负担,还可能导致搜索建议的显示出现延迟或混乱。使用防抖函数可以确保在用户停止输入后的一段时间内发送一次请求,从而提供更准确、更及时的搜索建议。
  4. 代码简洁和可维护性:防抖函数可以将复杂的防抖逻辑封装在一个函数中,使代码更加简洁和易于维护。开发者可以在需要防抖的地方直接调用这个函数,而无需在每个处理函数中编写防抖逻辑。
  5. 灵活性:防抖函数通常可以接受一些配置选项,如等待时间、是否立即执行等。这使得开发者可以根据具体需求灵活地调整防抖行为。

接下来小编就带大家来手搓实现一个防抖函数:

首先咱们来定义一个button按钮,为该按钮绑定一个监听事件,当点击该按钮的时候,就触发回调函数,在控制台输出'提交',代码实现如下:

<body>
    <button id="btn">提交</button>
    <script>
        let btn = document.getElementById('btn');
        function handle() {
            // ajax请求
            console.log('提交');
        }
        btn.addEventListener('click', handle);
    </script>
</body>

image-20240513121505157.png 如果咱们一直点击按钮,控制台就会一直输出,所以我们设置一个定时器,每隔1s再触发回调函数

function debounce(fn) {
    let timer = null;
    return function(){
        //如果第二次的时间没到1s,就销毁上一次的定时器
        clearTimeout(timer)
        timer=setTimeout(fn,1000)
    }
}

修改btn的绑定监听事件

btn.addEventListener('click', debounce(handle));

防抖函数里的闭包:

如果第二次点击的时间没到1s,就销毁上一次的定时器,这里咱们给定时器取一个变量名timer,初始值设置为null,每次触发点击事件的回调函数,先销毁上一次的定时器,再将新的定时器赋值给timer。细心的小伙伴可能就会发现上面这份代码就是一个典型的闭包应用。函数嵌套,对于debounce函数来说,function(){}函数天生就定义在debounce里面,但是它并不在debounce这个函数体里面调用,不在,它在外面btn的绑定监听事件中调用。所以这就是一个闭包的场景。当一个函数的内部函数,被返回到了这个函数外部去调用的时候,就会产生闭包。

防抖函数debounce执行完就会形成一个闭包,里面包含另一个子函数function(){}执行要引用的变量timer,当子函数执行需要用到timer时,会先去自己的执行上下文的词法环境中找,在去自己的变量环境找,找不到的话就会去debounce留下的闭包里找,直到找到timer为止。

三、防抖函数中的问题

实现防抖函数不难,难点是其存在两个问题,this指向问题和默认参数事件(e)被修改问题。

1.关于this的指向出错

原来的this是指向btn的,handle函数是由btn来绑定事件监听后点击事件触发调用的,所以是this中的隐式绑定,this指向btn.

 <script>
        let btn = document.getElementById('btn');
        function handle() {
            // ajax请求
           console.log('提交', this);
        }
         btn.addEventListener('click', handle);// this 指向btn
        // 防抖函数
        function debounce(fn) {
            let timer = null;  
            return function() {
             // 如果第二次的时间没到1s,就销毁上一次的定时器
            clearTimeout(timer);
            timer = setTimeout(function() { 
                fn();
               }, 1000);
            }
        }
    </script>

image-20240513172615781.png

但是看看现在的this指向,用debounce调用handle函数后的this指向,咱们输出this来看看:

let btn = document.getElementById('btn');
function handle() {
    //ajax请求
    console.log('提交',this);
}
btn.addEventListener('click', debounce(handle))
function debounce(fn) {
    let timer = null;
    return function () {
        clearTimeout(timer)
        timer = setTimeout(fn, 1000)
    }
}

image-20240513172251733.png 显然这不是我们想要的结果,原本this指向的就是btn,只不过由于要实现防抖功能把handle当成了参数传递给了debounce函数,因此handle里this指向发生了转变,设置防抖函数后this指向window:handle函数的调用是在定时器内的回调函数中调用的,定时器内的回调函数指向window,所以this指向window。为了修改回this正确的指向,我们有两种方法:

1、使用箭头函数和call

let btn = document.getElementById('btn');
function handle() {
    console.log('提交', this);
}

btn.addEventListener('click', debounce(handle))

function debounce(fn) {
    let timer = null;
    return function () {
        clearTimeout(timer)
        timer = setTimeout(() => {
            fn.call(this)
        }, 1000)
    }
}

改变this的指向,我们可以使用显示绑定,强行改变this的指向,发现debounce函数中的this指向的是btn按钮,箭头函数中没有this机制,所以箭头函数里的this,是外层函数debounce的,用call将fn中this的指向掰到了debounce函数中的this里,this的指向就正确了。

image-20240513190008636.png

还有一种方法,就是保存this,使用call,

let btn = document.getElementById('btn');
function handle() {
    console.log('提交', this);
}
btn.addEventListener('click', debounce(handle))
function debounce(fn) {
    let timer = null;
    return function () {
        const that = this
        clearTimeout(timer)
        timer = setTimeout(function () {
            fn.call(that)
        }, 1000)
    }
}

这里咱们没有使用箭头函数,也就是setTimeout(function () { fn.call(that)}, 1000)中函数里的this还是不指向debounce,因此不能直接用call改变this指向,但我们可以使用一个变量that来保存debounce里的this,再把变量that赋值到了fn中的this中。

2、默认参数事件(e)发生改变

在handle函数中,输出事件e

let btn = document.getElementById('btn');

function handle(e) {
    console.log('提交', e);
}

btn.addEventListener('click', debounce(handle))

function debounce(fn) {
    let timer = null;
    return function () {
        const that = this
        clearTimeout(timer)
        timer = setTimeout(function () {
            fn.call(that)
        }, 1000)
    }
}

输出: image-20240513231652977.png 好家伙,handle函数中的事件e又不见了???

看看debounce函数中有没有事件e:

let btn = document.getElementById('btn');

function handle(e) {
    console.log('提交', e);
}

btn.addEventListener('click', debounce(handle))

function debounce(fn) {
    let timer = null;
    return function (e) {
        console.log(e,'北京北京');
        const that = this
        clearTimeout(timer)
        timer = setTimeout(function () {
            fn.call(that)
        }, 1000)
    }
}

image-20240513232530330.png 好好好,这样玩是吧,直接跑到debounce函数里来了,我用个防抖函数结果把我默认事件参数(e)d都偷走了是吧?怎么修改呢?咱们可以用call将事件e拿回来:

let btn = document.getElementById('btn');

function handle(e) {
    console.log('提交', e);
}

btn.addEventListener('click', debounce(handle))

function debounce(fn) {
    let timer = null;
    return function (e) {
        console.log(e,'北京北京');
        const that = this
        clearTimeout(timer)
        timer = setTimeout(function () {
            fn.call(that, e)
        }, 1000)
    }
}

image-20240513235305549.png 好啦,今天的分享就到这里啦,快去手搓一个防抖函数试试吧!