堆栈数组区别-联想一系列名词

9 阅读8分钟

堆栈数组

是什么

  • 栈(Stack)线性、先进后出(LIFO) 的数据结构,操作仅在 “栈顶” 进行(入栈 push / 出栈 pop),无法随机访问。入栈 / 出栈 O (1),中间操作 O (n).

  • 堆(Heap)逻辑上是完全二叉树(不是内存分区的 “堆内存”),核心特性是 “父节点值≥/≤子节点值”(大 / 小顶堆),仅能快速访问堆顶极值。插入 / 删除(上浮 / 下沉)O (logn),找极值 O (1)

  • 数组(Array)线性顺序存储结构,底层是连续内存,通过下标 O (1) 随机访问,是栈 / 堆的常见物理实现载体(比如栈可基于数组实现,堆也常用数组映射树形结构)。尾部增删 O (1),中间增删 O (n),随机访问 O (1)

什么用-使用典型场景 “在前端开发中,这三者的应用能直接落地:

  1. 栈Stack:函数调用栈、历史记录回退、括号匹配、深拷贝递归、 JS引擎的函数调用栈(执行上下文入栈 / 出栈)、浏览器的历史记录回退(比如点击返回按钮,就是栈的 pop 操作)、算法题中的括号匹配(用栈校验是否成对)
  2. 堆Heap:TopK 问题、优先队列(任务调度)、堆排序 前端性能监控中统计「接口响应时间 Top5」(用小顶堆高效实现,不用全排序)、小程序 / React 的异步任务调度(优先队列基于堆实现,高优先级任务先执行);
  3. 数组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(); // 入口调用

执行流程(调用栈的变化):

  1. 初始状态:调用栈为空;
  2. 执行a():创建a的执行上下文 → 入栈(栈顶:a);
  3. a内调用b():创建b的执行上下文 → 入栈(栈顶:b);
  4. b内调用c():创建c的执行上下文 → 入栈(栈顶:c);
  5. c执行完毕:c的执行上下文 → 出栈(栈顶回到 b);
  6. b执行完毕:b的执行上下文 → 出栈(栈顶回到 a);
  7. a执行完毕:a的执行上下文 → 出栈(调用栈清空)。

控制台输出数序

a开始 → b开始 → c执行 → b结束 → a结束

函数调用栈只处理同步任务,异步任务(如 setTimeout、Promise)不会进入调用栈:

  • 同步函数调用:直接入栈执行;
  • 异步任务触发时:JS 引擎会把异步回调交给「任务队列」(宏 / 微任务),等待调用栈清空后,再通过事件循环将回调入栈执行。

js 运行机制 同步任务 异步任务

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

当主线程的代码执行完毕之后,在Event Loop执行之前,首先会尝试DOM渲染,这个时候,微任务是在DOM渲染之前执行,DOM渲染完成了之后,会执行宏任务。因此,微任务要比宏任务更早执行

  1. 为什么微任务要在 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 所指向的执行上下文中查找,把这个查找的链条就称为作用域链。