前言
这篇文章的主题是防抖与节流函数。将贴出实现代码,再从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