前言

在浏览器中,堆栈通常指的是调用栈(Call Stack)和堆(Heap)这两个核心概念,它们是 JavaScript 运行时环境中用于管理内存和执行代码的重要数据结构。

调用栈(Call Stack)

调用栈是一个后进先出(LIFO, Last In, First Out)的数据结构,用于管理函数调用的执行顺序。JavaScript 是一种单线程语言,意味着它一次只能处理一个任务,调用栈就是用来跟踪当前正在执行的函数以及函数调用链的。

工作原理

  • 每次调用一个函数,JavaScript 引擎会将该函数的执行上下文(Execution Context)推入调用栈。
  • 执行上下文包括函数的参数、局部变量、以及指向调用它的代码位置等信息。
  • 当函数执行完毕(返回结果),它的执行上下文会从栈顶弹出,控制权返回给上一个调用它的函数。
  • 如果栈中嵌套过深(例如递归调用没有终止条件),可能会导致栈溢出(Stack Overflow)错误。

示例

function greet() {
   sayHi();
}
function sayHi() {
   console.log("Hi!");
}
greet();

执行过程

  • greet() 被调用,推入调用栈。
  • 在 greet() 中调用 sayHi(),sayHi() 推入栈顶。
  • sayHi() 执行并打印 “Hi!”,完成后从栈顶弹出。
  • 控制权回到 greet(),执行完毕后 greet() 弹出。
  • 调用栈清空。

调用栈的查看

在浏览器开发者工具(DevTools)中,当代码抛出错误时,可以通过调用栈跟踪(Call Stack Trace)查看函数调用的层级关系,帮助调试。

堆(Heap)

堆是用于动态内存分配的内存区域,主要用来存储对象、数组等复杂数据结构。JavaScript 中的对象(如 {}、[])和闭包中的变量都会分配在堆中。

特点

  • 无序存储:与调用栈的严格 LIFO 结构不同,堆中的数据存储是无序的,分配和释放内存由 JavaScript 引擎的垃圾回收机制(Garbage Collection)管理。
  • 引用类型:堆中存储的对象通常通过引用(指针)被变量引用。例如,let obj = { key: ‘value’ }; 中,obj 是一个指向堆中对象的引用。
  • 垃圾回收:浏览器(如 V8 引擎)会定期扫描堆,回收不再被引用的内存,防止内存泄漏。

示例

let obj = { name: "Alice" }; // 对象 { name: "Alice" } 存储在堆中
let arr = [1, 2, 3];        // 数组 [1, 2, 3] 存储在堆中
  • obj 和 arr 是存储在调用栈中的变量,它们指向堆中的实际数据。

堆栈的关系

  • 调用栈负责函数的执行顺序,存储基本数据类型(如数字、字符串)和对堆中对象的引用。
  • 负责存储复杂数据类型(如对象、数组)。
  • 两者共同构成了 JavaScript 的内存模型,调用栈处理执行逻辑,堆处理数据存储。

浏览器中的事件循环(Event Loop)与堆栈

虽然调用栈是单线程的,但浏览器通过事件循环机制处理异步操作(如 setTimeout、Promise)。异步任务的结果会进入任务队列(Task Queue),事件循环会检查调用栈是否为空,若为空则从任务队列中取出任务推入调用栈执行。

示例

console.log("Start");
setTimeout(() => console.log("Timeout"), 0);
console.log("End");

执行过程

  • console.log(“Start”) 推入调用栈,执行后弹出。
  • setTimeout 推入调用栈,注册异步任务后弹出。
  • console.log(“End”) 推入调用栈,执行后弹出。
  • 调用栈为空,事件循环将 setTimeout 的回调函数推入调用栈,执行打印 “Timeout”。

总结

  • 调用栈:管理函数执行顺序,LIFO 结构,处理同步代码。
  • :存储复杂数据类型,由垃圾回收机制管理。
  • 浏览器通过调用栈、堆和事件循环协同工作,实现同步和异步任务的执行。
  • 如果调试时遇到问题,可以通过 DevTools 查看调用栈信息,分析函数调用链和内存使用情况。