从节流防抖到执行上下文、作用域链及闭包
从节流防抖到执行上下文、作用域链及闭包
本文深入探讨了前端开发中常见的节流(throttle)和防抖(debounce)概念,以及它们与执行上下文、作用域链和闭包的关系。通过详细的代码示例和应用场景说明,帮助读者理解这些技术在实际开发中的应用。
浅谈节流防抖
节流防抖在面试中可以算是高频出现
这两个概念也非常容易混淆
通常我们会描述一下节流防抖的具体概念,再讲一讲具体的应用场景,还可能遇到手写
下面我们来看看如何避免概念混淆
先上大白话概念:
- 一点击马上执行,在一段时间内(例如2秒)再次点击不会执行,那么这是什么?
- 点击后不会执行,开始计时(例如2秒),如果在2秒内继续点击,则重新计时,直到2秒完成,执行,这是什么?
从记忆角度,有以下说法
第一种就像 fps 游戏,即是一直按着鼠标,子弹也是按规定射速射出
相当于无限哒哒哒变成哒(空格)哒(空格)哒(空格)
那么有一个很形象的名字:节流
我们再来看看手写代码
// 节流函数
function throttle(func, delay) {
let lastTime = 0; // 用于记录上一次执行的时间
// 返回一个闭包函数
return function (...args) {
const now = Date.now(); // 获取当前时间戳
// 如果距离上次执行时间超过指定的延迟,则调用目标函数
if (now - lastTime >= delay) {
func.apply(this, args); // 使用 apply 确保正确的上下文
lastTime = now; // 更新上次执行时间
}
};
}
可以看到代码中使用了闭包,那么为什么要使用闭包呢?
谈到闭包就不得不谈一下作用域链,既然谈到作用域链就先从作用域说起,此时又需要引出执行上下文
执行上下文
执行上下文由变量对象、作用域链和 this 绑定三部分组成
首先需要知道,每个执行上下文(下面简称”上下文“)都有一个关联的变量对象
这个上下文中定义的所有变量和函数都存在这个对象上
全局上下文是最外层的上下文,在浏览器环境中全局上下文是 window 对象,通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法
注意:上下文在其所有代码都执行完成后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,如关闭网页或退出浏览器)
每个函数调用都有自己的上下文,当代码执行流进入函数时,函数的上下文会被推入一个上下文栈中,在函数执行完后,上下文栈会弹出这个函数上下文,此时控制权返回给之前的执行上下文
ECMAScript 程序的执行流就是通过这个上下文栈来控制的
ES5及之后规范中词法环境取代了变量对象
作用域链
上下文的代码在执行时,会创建变量对象的一个作用域链
注意:这里执行时的作用域链是基于定义时的词法作用域构建的
这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序
代码正在执行的上下文的变量对象始终位于作用域链的最前端
举个例子如下图所示,在 compare 函数的执行上下文中,compare 函数对变量对象是 0,在最前端
如上图所示,如果上下文是函数,变量对象是活动对象
活动对象最初只有一个定义变量:aruguments
作用域链中的下一个变量对象来自包含上下文,只不过上例直接来到了全局上下文(也就是作用域链中的1)
词法作用域和执行上下文的作用域链
作用域链和作用域是比较容易混淆的,实际上
执行上下文的作用域链是在执行时创建的,但它的结构(即变量查找顺序)是由词法作用域(定义时的嵌套关系)决定的
它们是在不同阶段的体现,作用域是在函数定义时确定的,执行上下文的作用域链是在函数调用时创建的
关键点:
2. 词法作用域决定了执行上下文的作用域链的结构
4. 执行时的作用域链是基于定义时的词法作用域构建的
6. 闭包之所以能访问外层变量,是因为执行上下文的作用域链保留了定义时的作用域关系
关键点1和关键点2看起来差不多,但实际上第一点强调设计规则(词法作用域是静态的),第二点则是实现方式(引擎如何执行该规则)
而上述关键就构成了闭包的基础
闭包
我们再回到闭包
闭包定义
用一句话表达:
闭包的是 某一个函数引用了另一个函数的作用域中的变量
也可以说闭包是 那些 引用了另一个函数作用域中变量 的函数
常见形式
在一个函数内部返回一个匿名函数,由匿名函数去访问上层作用域中的变量
经典代码
下面上一个闭包的经典代码:
function outer() {
let count = 0; // 外层作用域的变量
return function() { // 返回的匿名函数
count++; // 访问上层作用域的变量
return count;
};
}
const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
自由变量
在这里
count
就是匿名函数中的自由变量
因为它
- 不是匿名函数的参数
- 不是匿名函数内部声明的局部变量
- 是来自外层作用域的变量
作用域查找规则
自由变量的查找是在函数定义的地方向上级作用域查找,不是在执行的地方
给出的例子如下:
又一次验证了作用域链是基于定义时的作用域创建的
闭包本质
闭包的本质是上级作用域的变量由于被下级作用域引用,导致它们的生命周期延长,直到没有任何闭包函数再引用它时,这些变量才会被释放
闭包导致内存泄漏
不当使用可能导致内存泄漏,例如:
function createHeavyClosure() {
const bigData = new Array(1000000).fill("data"); // 大数据
return function() {
console.log("Closure holds bigData!");
};
}
const leak = createHeavyClosure(); // bigData 无法释放,直到 leak 被销毁
此时可以手动解除引用来解决内存泄漏
leak = null; // 释放闭包及其引用的变量
节流
现在我们回到节流
点我回到上面的核心函数
上文只给出了核心函数,下面在加上一个示例
再谈谈核心函数中的
...args
和
apply
...args
(剩余参数)的作用是接收任意数量的参数,无论原函数 func 需要多少参数,节流后的函数都能传给它
剩余参数
需要注意,在 apply 中用的是 args,因为 args 已经是一个数组,通过 ...args 收集而来
apply 的 this 指向由函数的调用方式来决定
例如上例就是
事件触发时:浏览器隐式调用
throttledScrollHandler
,并设置
this = window
(因为
addEventListener
的回调默认绑定到事件目标)
节流函数内部:
throttledScrollHandler
是
throttle
返回的闭包函数,它的
this
由调用方式决定(此处为
window
)
func.apply(this, args)
:将闭包的
this
(
window
)传递给原函数
handleScroll
节流定义
面试八股的时候可以讲的正式一些:
如果短时间内大量触发同一事件,在函数执行一次后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新被生效
应用场景
节流适用于频繁触发事件且多次执行事件处理函数的情况。当事件触发后,节流会在一定时间内等待用户操作,若在等待时间内再次触发事件,则忽略该事件。等待事件结束后,再执行一次事件处理函数。
- ✅监听页面的鼠标滚动事件
- 滚动会高频触发
scroll
事件[每秒几十次],可以设置 delay=100ms 避免频繁计算滚动位置 - ✅鼠标移动事件
- 和上面同理[每秒几十次],可以限制执行频率,如 delay=50ms
- 用户频繁点击按钮,只执行第一次
- 防抖:这里防抖可能更好,比如表单提交等避免重复提交
- 锁机制:第一次点击后禁用按钮,直到操作完成
- 搜索框input事件,要支持实时搜索可以使用节流方案
- 其实防抖更适合,比如 300ms 后再触发搜索,更节省资源
节流适合均匀控制执行频率,防抖适合避免中间状态
防抖
防抖呢,就像放技能的CD,再次点击技能的时候又会重新读条
防抖定义
短时间内大量触发同一事件,只会执行最后一次
在第一次触发事件时,不立即执行函数,而是给出一个限定值,比如300ms,然后
- 如果300ms内没有再次触发事件,那么执行函数
- 如果300ms内再次触发函数,那么当前的计时取消,重新开始计时
具体实现
// 防抖函数
function debounce(func, delay) {
let timer; // 定义一个定时器变量
// 返回一个闭包函数
return function (...args) {
const context = this; // 保存当前的 this 上下文
if (timer) clearTimeout(timer); // 如果定时器已经存在,则清除它
// 设置一个新的定时器,在 delay 时间后执行
timer = setTimeout(() => {
func.apply(context, args); // 使用 apply 确保正确的上下文
}, delay);
};
}
// 示例:为输入框添加防抖事件处理程序
const input = document.querySelector("#searchInput");
function handleInput(event) {
console.log("搜索内容:", event.target.value);
}
// 使用 debounce 函数创建一个防抖的处理程序
const debouncedInputHandler = debounce(handleInput, 500);
// 将防抖后的处理程序应用到输入框的输入事件上
input.addEventListener("input", debouncedInputHandler);
在这个代码中 setTimeout 用的是箭头函数,所以其实 apply 中的 context 替换成 this 也是可以的,如果普通函数,this 会指向 windows,此时 context 就是必要的
应用场景
防抖适用于频繁触发事件的请求。当事件触发后,防抖会在一定时间内等待用户操作,若在等待时间再次触发事件,则重新计时。等待事件结束后,才会执行一次事件处理函数。
- ✅搜索框输入,input 实时搜索
- 输入框中频繁的输入内容,停止输入内容才进行搜索
- 如果每次输入都请求搜索接口,会导致大量无效请求
- ✅用户缩放浏览器的resize函数,窗口大小发生改变
- 用户调整浏览器窗口时,
resize
事件会高频触发(每秒几十次) - 如果每次
resize
都重新计算布局,会导致性能问题,防抖避免频繁重绘 - ✅表单提交
- ✅自动保存
- 比如富文本编辑器
END
作用域和 this 指向本文还没细讲,先挖个坑
如果有错误请评论,感谢支持🤝