Once、throttle、debounce函数解析

88 阅读9分钟

在 JavaScript 中,闭包(Closure)和高阶函数(Higher-order Functions)都是函数式编程的重要概念,它们在多种编程范式中起着核心作用,尤其是在使代码更灵活、模块化和可重用方面。

函数闭包

在讲高级函数之前不得不讲一下函数的闭包,我们所说的高级函数都是基于函数的闭包封装好的函数。

闭包

对于闭包的定义: 闭包是指有权访问另外一个函数作用域中的变量的函数

闭包 - JavaScript | MDN

闭包延长了生命域周期,且外部是无法对闭包里面缓存的值进行直接访问的。通俗来说,在闭包函数里有一个可以缓存的有独立作用域的函数。我们简单举个例子:

function demo(){
  let number = 0
  return function(){
    console.log(number++)
  }
}
let f1 = demo()
f1()
f1()
f1()
// console.log(number)

运行结果:

其中我创建一个了一个函数demo返回了一个函数。当我每次执行f1的时候,输出的结果都是demo函数中的number。解释一下刚刚我说的闭包的特点:

  • 可缓存:就在这个例子中可以很明确的看到输出的值并不像普通的函数一样,每次执行函数输出的结果都是一样的;而是在闭包函数里面,记录着上一次的执行状态/结果。也就是将数据进行了缓存
  • 独立作用域:如果将我最后一行代码取消注释的话,很明显是访问不到demo函数里面的number值的,这个是由函数的特性决定的

高级函数入门

高阶函数是指那些接受函数作为参数或将函数作为返回结果的函数。这种函数的存在使得 JavaScript 程序可以将函数作为模块单元,传递和返回,从而提高代码的复用性和抽象性。

这个概念很抽象,简单点说和闭包的去比诶就是,高级函数的入参是函数,返回的也是一个参数。下面就是一个非常简单的例子,让我们认识一下高级函数,好介绍下一个部分的常用的高级函数。

function fun(msg){
    console.log(`log - ${msg}`)
    return this.helloMsg
}
//fn是一个函数
function fun2(fn){
    this.helloMsg = "this.helloMsg"
    //...agr接收的是fn传入的参数
    return function (...agr){
        //显示将fun2中的作用域显示绑定this给fn
        //执行函数,ans就是fn的返回值(如果有返回值的话)
        let ans =  fn.apply(this, agr)
        return ans  
    }
}

let f = fun2(fun)
console.log(f("msg"))

输出结果:

fun就是一个简单的函数,输入一个字符串打印出来,然后返回一个字符串。

fun2 是封装的一个最简单的高级函数。这个fun2的入参是一个函数,然后执行fn,返回结果。对于封装fun2:

  • fn:入参是一个函数(不是返回结果)
  • apply:显示绑定this并且执行函数。
    • this绑定:绑定的这个this是fun2中this,也就是说在fn中可以使用fun2的上下文
    • agr:上面也说了,fun2的入参是一个函数,而不是函数结果。如果执行fn需要参数(如f("msg")中的"msg"),就是由fun2中返回的函数接收,并且使用...这个扩展运算符,接受从零到多个参数,并且将它们作为一个数组处理

调用函数:

  • f: 通过调用 fun2(fun) 得到的结果,它实际上是 fun2 返回的那个新函数的一个实例。这个新函数保留了对 funfun2 中定义的变量的访问,这是通过 JavaScript 的闭包机制实现的。
  • fun - 函数 + fun2 - 含有闭包的函数 = f - 函数 (之所以会得到一个函数因为fun2返回的就是一个匿名函数,在这个匿名函数中会执行fun)。f这个函数的执行会间接的执行fun函数,并且使用的是有fun2上下文的作用域

上面这个高级函数其实并没有解决什么实际上的问题,只是想通过这个例子来引入这么个高级函数的模板。

高级函数的清除/创建

前面也说了,高级函数其中的闭包部分可以让我们缓存数据,就比如说demo里面的number,假如需要重置或者说清空这个缓存呢?当然你直接let f2 =demo()直接创建一个新的函数实例也可以,创建了一个新的缓存。

要“删除”已有的缓存,直接将已有的缓存置空f1 = null,就可以完成任务。

常用高级函数

Once

使用场景:在封装一些删除的按钮的时候,经常会出现一个问题:会因为时延或者组件本身有动画,导致用户点击与删除提示不同步(我点击一下没反应,然后多点击几遍)。但是除了第一次之外,后续的点击其实删除的是空对象,这样的行为会出现很多奇怪的bug。

我们封装Once函数就可以实现,通过高级函数Once调用的fn函数只会在函数实例中执行一次。

function Once(fn){
    return function (...agr){
        //如果是第一次执行fn并没有被置空,继续执行
        if(fn){
            let ans = fn.apply(this, ...agr);
            // 执行过一次之后将fn置空,这样在Once的作用域中fn之后一直都将是null
            fn = null;
            return ans;
        }
    }
}

运行结果:

解析

在创建实例deleteFunction之后,在作用域内,fn就是创建实例时传入的函数fun。当deleteFunction第一次被执行时,fn是可以正常被执行的。在执行完之后,我们要完成“第一次执行之后都不执行”这个需求,我们就可以在Once的作用域内将fn指针指向null。

当第二次或者更多次被执行时,在deleteFunction作用域内fn = null,所以不会执行fun。

throttle(节流函数)

使用场景:可以说是说有的按钮,应该都有这个功能:一段时间内,多次点击,只执行一次。

有了Once函数的基础,我们直接说一下throttle的核心是啥:最近一次目标函数被执行后,在一定的时间内不允许再次执行;用计时器的状态限制目标函数的执行

function debounce(fn,time){
    let timer = null
    return function(...agr){
        if(!timer){
            // 第一次执行 or 已经超过了延迟时间,将timer清除后
            fn.apply(this, agr)
            // 执行函数之后使用计时器,等待延迟时间,将timer清除,再次执行
            // 在延迟时间内被调用,timer != null,就不会执行
            timer = setTimeout(()=>{
                timer = null;
            },time)
        }

    }
}
// 我们假设登录请求1.5秒内只能发送一次请求
let loginBtnFunction = debounce(fun,1500)

执行结果:

解析

起初在debounce作用域中timer = null,当fn被执行之后,将timer指向一个单次计时器。如果这个计时器不结束,即使你多次调用,在debounce函数的作用域中timer都不为空(指向那个正在计时的计时器);当计时器结束,将timer清除之后,函数再次被调用,就可以再次执行。如第二次输出发送请求。

debounce(防抖函数)

使用场景:在一些自动保存的组件在中比较常见。比如,对于一个在线文本编辑器来说,在用户进行修改之后我需要进行保存,执行保存这个操作的策略最常见的就是定时保存:每五秒保存一次。对于前端来说没什么问题,但是定时提交数据,对于后端服务器而言,那就是问题很大了,如果用户真的没五秒钟都进行修改,做了有效操作还好,但是如果用户在这五秒并没有操作呢?

另外一种保存的策略就是,在用户进行修改之后进行保存。如果不加以其他的操作,用户每修改一个内容就想向后端发送数据,服务器更加崩溃(服务器:我可没惹你们,你们为什么要这么对我~/~呜呜)。所以需要使用到debounce这个函数。减少前端操作的频率。

防抖函数的bounce的作用:当被调用不会立刻执行,在n秒内不再被调用,才执行。延伸到上面那个自动保存的例子就是:我修改了文本但是n秒内我再次修改,就不会执行自动保存;如果我进行修改结束之后n秒再保存(如果一直发生改变,不执行目标函数;变化停止,再执行)

// 在构造实例需要传入延迟时间:延迟多久之后执行函数
function throttle(fn,time){
    let timer = null
    return function(...agr){
        if(!timer){
            timer = setTimeout(() => {
                timer = null;
                fn.apply(this,agr)
            }, time);
        }
    }
}

解析

有了上面的Once函数的基础,我们可以直接尝试理解throttle函数的核心在哪:每次被调用都在维持一个定时器的状态。

在throttle的实例创建之初,timer = null然后启动一个计时器进行等待。当等待时间还没有结束时,再次被调用,计时器将被清除,然后再次启动计时器等待。如此反复.....

当最后一次被调用并且计时器结束之后,目标函数才会执行

总结

从上面的高级函数的例子中,我们其实可以看到,高级函数实现的功能都是依靠闭包中的缓存特性,根据上一次执行的结果,来判断下一次函数的执行情况。

  • Once():执行过一次之后缓存fn -> null,之后不再执行
  • throttle():缓存timer这个计时器,当timer -> null才可以执行;当timer指向一个定时器的时候,就无法再次执行
  • debounce():也是缓存timer,只有完整等待timer指向的定时器结束之后才能执行;等待期间再次被调用,只会清空之前等待的时间(重启计时器)

GPT:在函数式编程中,闭包和高阶函数使得编写无副作用的代码(即不改变外部状态的代码)更为简单。它们也有助于实现如延迟计算和函数组合等高级编程技术。这些特性使得代码更加模块化,增加了代码的可测试性和可维护性。