前言
在浏览器中,堆栈通常指的是调用栈(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 查看调用栈信息,分析函数调用链和内存使用情况。