前言

这篇文章的主题是防抖与节流函数。将贴出实现代码,再从JS语言特性的角度,以防抖函数为例,尝试解释其中个人认为比较难理解的点,涉及执行上下文、作用域链、闭包、箭头函数、apply、this等知识。水平有限,若有纰漏,恳请指正。

防抖

代码实现

function debounce(fn) {
    let timeout = null; //私有变量,存放定时器的返回值
    return function() {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            fn.apply(this, arguments);
        }, 500);
    };
}

function sayHi() {
    console.log('防抖成功');
}

var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi));

函数的创建和执行

  • 函数声明语句function debounce(..) {...}创建了一个debounce函数,保存作用域链到其内部属性[[scope]]
    debounce.[[scope]] = [
      globalContext.VO //其实就是全局对象
    ]
  • 函数调用表达式 debounce(sayHi)执行debounce函数,创建debounce函数执行上下文,压入执行上下文栈
    ECStack = [
      debounceContext
      globalContext
    ]
  • 函数并不立即执行,开始准备工作,第一步:复制函数[[scope]]属性创建作用域链
    debounceContext = {
      scope: debounce.[[scope]]
    }
  • 准备工作第二步:用arguments实参对象初始化活动对象AO,然后,加入形参、函数声明、变量声明
    debounceContext = {
      AO: {
          timeout:undefined
      }
      scope: debounce.[[scope]]
    }
  • 准备工作第三步:将活动对象压入debounce作用域链顶端
    debounceContext = {
      AO: {
          timeout:undefined
      }
      scope: [AO, debounce.[[scope]]]
    }
  • 准备工作做完,开始执行函数,随着函数的执行,修改AO的属性值
    debounceContext = {
      AO: {
          timeout:null
      }
      scope: [AO, debounce.[[scope]]]
    }
  • 函数定义表达式function(){...}创建了一个匿名函数(下文称为anonymous),保存作用域链到其内部属性[[scope]]
    anonymous.[[scope]] = [debounceContext.AO, debounce.[[scope]]]
  • debounce函数返回匿名函数后执行完毕,函数上下文从执行上下文栈弹出。

私有变量timeout

debounce(sayHi)返回匿名函数,作为input事件的回调函数被绑定到input元素上,因为debounceContext.AO仍被匿名函数的[[scope]]引用着,所以没有被内存释放,其中的timeout变量也因此能被匿名函数访问。当需要查找timeout变量时,匿名函数首先到自己的AO查找,若没有,就循着[[scope]]指向的作用域链逐一查找。

setTimeout

每次触发事件执行anonymous,首先清除上次的setTimeout任务,开始新的setTimeout任务。setTimeout里的回调函数如果是普通函数,那么该回调函数的this始终指向window,即全局对象。

可这里是箭头函数

箭头函数不会创建自己的this,都是继承作用域链中上一层的this。[4]

(偷偷附带一个知识点:函数里如果有嵌套函数,这个嵌套函数的this默认是全局变量。这里还有一个疑问,根据《JavaScript深入之作用域链》,函数作用域链中保存的是每个函数的AO,不包含this,this应该保存在函数执行上下文中,那么箭头函数是如何继承this的?)

通常来说this的值是触发事件的元素的引用。[5]

这也许就不能算个问题,答案很简单:箭头函数就是能继承上一层作用域的this。也许这是编译器的工作(编辑于2020/5/26)

箭头函数作用域链的上一层是anonymous函数,其this指向绑定的input元素,所以箭头函数中的this也应是绑定的元素。箭头函数也没有自己的arguments,而是使用封闭作用域内的arguments,所以arguments使用anonymous函数的实参对象arguments。fn.apply(this, arguments)的写法,是为了能获取绑定元素input的属性(譬如可以使用this.value获取input元素的输入值),以及从外部传递参数。

节流

节流函数的细节类似,不再赘述。参考代码如下:

function throttle(fn) {
    let canRun = true;
    return function() {
        if (!canRun) return;
        canRun = false; //
        setTimeout(() => {
            fn.apply(this, arguments);
            canRun = true;
        }, 500);
    };
}

function sayHi(e) {
    console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));

最后

如果要给不同的回调函数防抖/节流,只要同样地套一个防抖/节流函数就行了。防抖函数debounce的timeout变量是共享的吗?其实即便是同一函数,每执行一次,都会产生不同的执行上下文,存储变量的AO也不同。el.addEventListener('event', debounce(callbackA))debounce返回的anynomousA作为el的回调函数。debounce的这次执行产生的AO里的timeout只有anynomousA能访问到。

参考链接

[1]https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/5
[2]https://github.com/mqyqingfeng/Blog/issues/6
[3]https://developer.mozilla.org/zh-CN/docs/Web/API/Window/setTimeout#%E5%85%B3%E4%BA%8Ethis%E7%9A%84%E9%97%AE%E9%A2%98
[4]https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions#没有单独的this
[5]https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener#The_value_of_this_within_the_handler