44 JS闭包+防抖/节流函数实现

360截图20220505215822071

  • 这里结合防抖,节流函数实现去解释闭包(因为两者都用到了)
  • 防抖
 // function debounce(val,delay) {//想要实现防抖就需要令timer在debounce函数内为全局变量
    //     let timer;
    //     clearTimeout(timer)//这里无效是因为每一次触发事件都会重新声明timer(undefined)
    //     console.log(timer);
    //     timer = setTimeout(()=>{
    //         console.log(timer);
    //     },delay)
    // }

    function debounce(delay,callback) {//想要实现防抖就需要令timer在debounce函数内为全局变量

        let timer;
        console.log(111,timer)
        return function (val) {
            console.log(222,timer)
            clearTimeout(timer)//原理借助同步比异步程序快的特点,删除上一次生成的timer定时器,只按最后一个执行
            timer = setTimeout(function () {
                callback(val)
            },delay)
        }
    }

    function f(val) {
        console.log(val)
    }
    let t = debounce(1000,f);
    document.querySelector('.debounce').addEventListener('keyup',(event)=>{
        t(event.target.value)
        // debounce(1000,f)(event.target.value)
    })
  1. 结合图片去类比理解:变量t = 图片变量bag,因此键盘事件的回调 = bag()

  2. 调用栈执行到let t = debounce(1000,f);时,为debounce函数创建执行上下文,创建作用域链:里面首先放的是自己的变量对象,(自己的变量对象里有变量timer,形参delay,callback),接着就是全局变量对象(t,debounce)

  3. 此时t得到的就是返回的匿名函数,当键盘事件触发,t就会为匿名函数创建执行上下文,创建作用域链,自己的变量对象就是传入的形参val,像图里那样放在链表的首位

  4. 执行setTimeout,因为是异步任务,所以执行完的结果是添加在消息队列上的,(只有调用栈清空才会对消息队列轮询,在设置好的时间间隔delay来开始执行消息队列里的任务,加入到调用栈中执行处理)待t对应的匿名函数执行完毕后,返回一个id给timer

  5. 此时如果键盘事件只触发一次,调用栈为空就将消息队列里的setTimeout里的匿名函数调入栈执行,创建的执行上下文是连着t对应的匿名函数的作用域链的,因此我们可以通过他找到变量val,函数callback,执行输出结果

  6. 但键盘事件如果是多次触发,我们的t对应的匿名函数虽然执行完后从内存里销毁,但timer这个关键变量上一次保存的值还是存在的,因此再次调用时,会从作用域链里找回timer,然后将其异步任务删去,重新放一个新的异步任务进消息队列,因此抖动的特点就是:

    1.当触发停止一段时间后,我们才执行逻辑代码

    2.当持续触发事件时,我们不执行逻辑代码

  • 节流
  let t
  function thoretle(func,wait) {
      let timeout
      console.log(111,timeout)

      return function () {
          console.log(222,timeout)
          if (!timeout){//wait时限内只允许第一个人进来,之后的都会被拦截
              timeout = setTimeout(()=>{//,因为定时器会返回一个id给timeout,再执行异步操作
                  func()
                  timeout = null//执行完异步操作在调整成空,为下一个人进来准备
              },wait)
          }
      }

      // if (!t){  //可以使用全局变量,但是会有变量污染的风险,同时没有闭包也不利于函数的封装
      //     t = setTimeout(()=>{
      //         query()
      //         t = null
      //     },1500)
      // }
  }
  function query() {
      console.log(Math.random())
  }
    //函数执行完返回一个函数交由click事件调用
    //这样该函数的变量timeout将类似thoretle函数内的全局变量
  document.querySelector('.btn').onclick = thoretle(query,1500)
  1. thoretle(query,1500)返回一个回调函数交由click自己调用,事件发生时,实际上这个匿名函数的作用域链也是连接在了thoretle变量对象上,因此我们可以访问到timeout变量,第一次undefined时就可以执行异步任务了,但之后反复触发都是有效值因此都会无功而返,知道timeout再次为null才能执行

总结

借助作用域链+闭包的做法,我们可以将外层函数的变量保存起来交由我们全局作用域下直接调用/修改,避免了全局变量的污染

作用域链,闭包理解补充