堆栈数组
是什么
-
栈(Stack) :线性、先进后出(LIFO) 的数据结构,操作仅在 “栈顶” 进行(入栈 push / 出栈 pop),无法随机访问。入栈 / 出栈 O (1),中间操作 O (n).
-
堆(Heap) :逻辑上是完全二叉树(不是内存分区的 “堆内存”),核心特性是 “父节点值≥/≤子节点值”(大 / 小顶堆),仅能快速访问堆顶极值。插入 / 删除(上浮 / 下沉)O (logn),找极值 O (1)
-
数组(Array) :线性顺序存储结构,底层是连续内存,通过下标 O (1) 随机访问,是栈 / 堆的常见物理实现载体(比如栈可基于数组实现,堆也常用数组映射树形结构)。尾部增删 O (1),中间增删 O (n),随机访问 O (1)
什么用-使用典型场景 “在前端开发中,这三者的应用能直接落地:
- 栈Stack:函数调用栈、历史记录回退、括号匹配、深拷贝递归、 JS引擎的函数调用栈(执行上下文入栈 / 出栈)、浏览器的历史记录回退(比如点击返回按钮,就是栈的 pop 操作)、算法题中的括号匹配(用栈校验是否成对)
- 堆Heap:TopK 问题、优先队列(任务调度)、堆排序 前端性能监控中统计「接口响应时间 Top5」(用小顶堆高效实现,不用全排序)、小程序 / React 的异步任务调度(优先队列基于堆实现,高优先级任务先执行);
- 数组Array:存储列表数据、渲染 DOM 列表、接口数据处理 日常开发中存储接口返回的列表数据(比如
list: [])、渲染 Vue/React 的列表(v-for/map遍历数组)、处理表单数据等,是前端最基础的存储结构。”
上面堆栈数组引出的一些知识点, 解释知识点会引起新的知识点依次往下写
JS引擎的函数调用栈(执行上下文入栈 / 出栈)
“函数调用栈(Call Stack)是 JS 引擎管理函数执行的后进先出(LIFO) 线性数据结构,本质是「存储当前执行上下文的栈」—— 每调用一个函数,JS 引擎就会创建该函数的「执行上下文」(包含作用域、参数、变量、this 等)并入栈;函数执行完毕,其执行上下文会出栈;主线程始终执行栈顶的执行上下文。
简单说,调用栈就像「叠盘子」:新调用的函数是新盘子(压在最上面),执行完就拿走(出栈),永远只操作最顶端的盘子,这也是单线程 JS 保证执行顺序的核心。”
函数调用栈的执行流程(结合代码示例,更直观)
用一段简单代码拆解执行过程,面试中画个简易栈结构更加分:
function a() {
console.log('a开始');
b(); // 调用b
console.log('a结束');
}
function b() {
console.log('b开始');
c(); // 调用c
console.log('b结束');
}
function c() {
console.log('c执行');
}
a(); // 入口调用
执行流程(调用栈的变化):
- 初始状态:调用栈为空;
- 执行
a():创建a的执行上下文 → 入栈(栈顶:a); a内调用b():创建b的执行上下文 → 入栈(栈顶:b);b内调用c():创建c的执行上下文 → 入栈(栈顶:c);c执行完毕:c的执行上下文 → 出栈(栈顶回到 b);b执行完毕:b的执行上下文 → 出栈(栈顶回到 a);a执行完毕:a的执行上下文 → 出栈(调用栈清空)。
控制台输出数序
a开始 → b开始 → c执行 → b结束 → a结束
函数调用栈只处理同步任务,异步任务(如 setTimeout、Promise)不会进入调用栈:
- 同步函数调用:直接入栈执行;
- 异步任务触发时:JS 引擎会把异步回调交给「任务队列」(宏 / 微任务),等待调用栈清空后,再通过事件循环将回调入栈执行。
js 运行机制 同步任务 异步任务
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
当主线程的代码执行完毕之后,在Event Loop执行之前,首先会尝试DOM渲染,这个时候,微任务是在DOM渲染之前执行,DOM渲染完成了之后,会执行宏任务。因此,微任务要比宏任务更早执行
- 为什么微任务要在 DOM 渲染前执行?
(1)保证数据变更能及时影响 DOM
微任务通常用于 高优先级回调,例如:
Promise.then(异步操作完成后的回调)。
MutationObserver(监听 DOM 变更,避免频繁重绘)。
如果微任务在 DOM 渲染后 执行,可能会导致:
数据已经变更,但 UI 还未更新(视图不一致)。
用户看到旧数据,然后突然刷新(视觉闪烁)。
(2)避免不必要的渲染
浏览器会尽量合并 DOM 变更,减少 重排(Reflow)和重绘(Repaint)。
微任务执行完后,浏览器可以一次性计算最新的 DOM 状态,然后只渲染一次。
如果微任务在渲染后执行,可能会导致多次渲染(性能下降)。
宏任务会无限执行(直到页面关闭或内存耗尽)
一、同步和异步:
所有的线程,都是有同步队列,和异步队列,立即执行的任务队列,这些都是属于同步任务,比如一个简单的函数;请求接口发送ajax,发送promise,或时间计时器等等,这些就是异步任务。
二、任务队列-事件循环:
同步任务会立刻执行,进入到主线程当中,异步任务会被放到任务队列当中。
等待同步代码执行完毕后,返回来,再将异步中的任务放到主线程中执行,反复这样的循环,这就是事件循环。
也就是先执行同步,返回来按照异步的顺序再次执行。
三、宏观任务和微观任务(先执行微观任务,再执行宏观任务)
在事件循环中,每进行一次循环操作称为tick,tick 的任务处理模型是比较复杂的,里边有两个词:分别是 Macro Task (宏任务)和 Micro Task(微任务)。
简单来说:
宏观任务主要包含:setTimeout、setInterval、script(整体代码)、I/O、UI 交互事件、setImmediate(Node.js 环境)
微观任务主要包括:Promise、MutaionObserver、process.nextTick(Node.js 环境)
规范:先执行微观任务,再执行宏观任务
那么我们知道了,Promise 属于微观任务, setTimeout、setInterval 属于宏观任务,先执行微观任务,等微观任务执行完,再执行宏观任务。
原文链接:blog.csdn.net/weixin_4565…
2 执行上下文 作用域 作用域链
JavaScript标准把一段代码(包括函数)执行所需的所有信息定义为“执行上下文”(可理解为当前代码的执行环境,同一个函数在不同的环境中执行,会因访问的数据不同产生不一样的结果),其是执行的基础设施。
全局执行上下文: 当JavaScript执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
函数执行上下文: 只有当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。需要注意的是,同一个函数被多次调用,都会创建一个新的上下文
作用域
就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
在 ES6 之前,ES 的作用域只有两种:全局作用域和函数作用域。
全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
作用域链
在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为outer。
当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量,如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找,把这个查找的链条就称为作用域链。