你是否在面试中被问过:JavaScript 的基本类型和引用类型存储在哪里?你是否遇到过页面越用越卡、内存持续飙高,却找不到原因?你是否理解闭包、浅拷贝、内存泄漏和栈堆的底层关系?
在前端开发中,栈(Stack)与堆(Heap)是 JavaScript 内存模型的核心基石,也是面试高频考点、性能优化的关键。很多同学只知其名,不知其理,导致在实际开发中踩坑无数。
今天,我们就结合你的核心知识点,从零到一吃透 JS 栈堆内存、垃圾回收、内存泄漏、闭包 等硬核知识点,全文逻辑闭环、内容详实,无论是面试还是实战优化都直接能用。
一、开篇三问:你真的了解 JS 内存吗?
在深入栈堆之前,我们先抛出三个灵魂问题,带着问题学习更有方向:
- 为什么
let a = 1和let a = {}赋值、拷贝的表现完全不同? - 为什么函数执行完,局部变量就消失了,而闭包变量能一直保留?
- 为什么项目跑久了会卡顿?内存泄漏到底和栈堆有什么关系?
这三个问题的答案,全部藏在 JavaScript 的栈堆内存模型 里。接下来我们逐层拆解,从内存分类、存储规则、管理方式,到垃圾回收、内存泄漏,一次性讲透。
二、第一部分:JavaScript 内存模型 —— 栈与堆
在 JavaScript 中,引擎会把内存划分为 ** 栈内存(Stack)和堆内存(Heap)** 两部分,二者分工明确、各司其职,共同支撑 JS 代码的运行。
1. 栈内存(Stack):系统自动管理的高速内存
栈内存是 JavaScript 中执行代码、存储简单数据的核心区域,它的管理方式完全由系统自动完成,开发者无需手动干预。
栈内存存储什么?
- 执行上下文(全局上下文、函数执行上下文)
- 基本数据类型(Number、String、Boolean、Null、Undefined、Symbol、BigInt)
- 函数调用记录(调用栈)
- 引用类型的内存地址(指针)
栈内存核心特点
- 操作速度极快栈是 CPU 最友好的内存结构,读写只需要移动指针,效率远高于堆内存。
- 内存空间连续栈内存是一块连续的存储空间,遵循 LIFO(后进先出) 原则,和数据结构中的栈完全一致。
- 系统自动分配与释放函数执行时入栈,执行完毕后,栈内数据自动出栈销毁,不需要垃圾回收机制(GC) 参与。
- 存储空间小、固定大小栈内存大小有限,不适合存储大型、复杂的数据。
一句话总结栈内存:小而快、自动管、存简单值 / 地址。
2. 堆内存(Heap):GC 管理的动态内存
堆内存用于存储复杂、占用空间大的数据,它的管理方式和栈内存完全不同。
堆内存存储什么?
- 对象(Object)
- 数组(Array)
- 函数(函数的调用逻辑存在栈中,函数体本身存在堆中)
- 所有引用类型的真实数据
堆内存核心特点
- 内存空间不连续堆内存是散乱分配的,动态申请空间,会产生内存碎片。
- 操作速度较慢分配和回收都需要计算,读写效率低于栈。
- 手动 / GC 自动管理底层语言(C/C++)需要
new申请、delete释放;JavaScript 不需要手动操作,由 GC(垃圾回收机制) 自动管理。 - 存储空间大、动态大小可以存储任意大小的复杂数据。
一句话总结堆内存:大而慢、GC 管、存真实对象数据。
3. 栈与堆的协作关系
在 JavaScript 中,变量本身在栈中,而对象实际存在堆中,栈里存的是指向堆的引用地址。
举个最经典的例子:
// 基本类型:直接存在栈内存
let num = 100;
let str = "前端";
// 引用类型:栈存地址,堆存真实数据
let obj = { name: "掘金" };
let arr = [1, 2, 3];
执行这段代码时:
- 栈内存:存储
num、str的值,存储obj、arr的堆内存引用地址 - 堆内存:存储
{name:"掘金"}、[1,2,3]的真实数据
这就是 JS 内存最核心的规则:栈里存的是简单类型值 或 引用地址;堆里存的是对象的真实数据。
4. 栈与堆性能差异深度对比
- 栈快(只需要挪指针),堆慢
- 栈连续内存(LIFO,无需 GC),堆非连续(随机分配,需要 GC)
- 闭包,本该在栈释放的数据,被引用到了堆中
从生命周期来看:栈中的数据随着函数执行结束自动释放,而堆中的数据只要引用存在就不会被回收。
三、第二部分:数据结构中的栈与堆
除了内存模型,栈和堆也是数据结构中的常客,面试中经常会把「内存栈堆」和「数据结构栈堆」放在一起问,我们必须区分清楚。
1. 数据结构中的栈
栈是一种线性数据结构,严格遵循 LIFO(Last In First Out,后进先出) 原则。
- 只能从一端添加 / 删除数据(栈顶)
- 经典应用:函数调用栈、括号匹配、浏览器后退功能
它和内存中的栈内存规则完全一致,这也是栈内存得名的原因。
2. 数据结构中的堆
堆是一种非线性的树形数据结构,和内存中的堆完全不是一个概念!
数据结构中的堆分为两种:
- 大顶堆:父节点值 ≥ 子节点值
- 小顶堆:父节点值 ≤ 子节点值
堆常用于:优先队列、堆排序、TOP K 问题。
⚠️ 重要区分:
- 内存堆:存储引用类型,GC 管理
- 数据结构堆:排序、优先队列
面试中如果同时问到,一定要清晰区分二者,不要混淆。
四、第三部分:JS 垃圾回收机制(GC)—— 内存的 “清洁工”
堆内存需要垃圾回收机制来释放空间,GC 就是 JS 引擎的内存清洁工,负责把「不再使用的对象」清理掉,释放内存。
1. 对象可回收的核心标准:是否可达
GC 判断一个对象是否能被回收,只有一个标准:这个对象是否还能被访问到?(是否可达)
只要对象无法通过任何方式被访问,GC 就会标记它,并在合适时机回收内存。
我们用四个经典案例,彻底讲透「对象可达性」:
案例 1:局部对象 —— 自动回收
function fn(){
let obj={a:1} //会被回收
}
fn();
函数执行完毕,执行上下文出栈,obj 变量销毁,堆中对象无引用 → 不可达 → 被回收。
案例 2:全局引用 —— 不会回收
let globalObj;
function fn(){
let obj={a:1} //对象引用 n=1 不会释放
globalObj=obj; //对象引用 n=1+1
}
fn(); //n-1=1
函数执行完,局部变量 obj 销毁,但 globalObj 仍在引用,对象始终可达 → 不会被回收。
案例 3:循环引用
let a={};
let b={};
a.x=b;
b.y=a;
// 循环引用
a=null;
b=null;
老式引用计数算法会因计数不为 0 无法回收,造成泄漏;现代标记清除算法会判断不可达,正常回收。
案例 4:闭包
function outer(){
let obj={a:1}
return function inner(){
console.log(obj)
}
}
let fn=outer();
内层函数引用外层变量,导致 obj 一直可达,不会被回收。这就是闭包的本质:栈上本该释放的变量,被引用到了堆中,延长了生命周期。
2. 垃圾回收算法
(1)引用计数
- 被引用一次 +1,取消引用 -1
- 计数为 0 则回收
- 致命缺陷:循环引用无法回收
- 现已基本废弃
(2)标记清除(V8 主流)
- 从根对象(window/global)开始遍历
- 给可达对象打标记,未标记对象清除
- 解决循环引用问题
- 缺点:产生内存碎片
通俗理解:打扫卫生,哪些要断舍离呢?贴个标签,没贴的就扔掉回收。
五、第四部分:内存泄漏 —— 前端的 “隐形杀手”
内存泄漏是前端应用中的隐形杀手。本该被回收的内存,因被意外引用而无法释放,导致占用持续增长,最终引发页面卡顿甚至崩溃。
它不会立即导致应用崩溃,而是像慢性病一样,随着用户使用时间的延长,逐渐吞噬系统资源,最终导致卡顿、延迟,甚至浏览器标签页崩溃。
下面我们逐一梳理最常见的 9 种内存泄漏场景,并给出解决方案。
1. 定时器未清理
React / Vue 组件卸载了,但定时器没有取消。定时器内部函数引用了外部变量,变量被长期引用,无法释放。
解决方案:组件卸载时清除定时器。
2. 事件监听未移除
给 window、document、DOM 绑定事件后,组件销毁未解绑,回调函数长期驻留内存。
3. DOM 引用未释放
let el=document.getElementById('app')
document.body.removeChild(el);
// DOM 已删,但引用还在
el=null; // 必须手动切断
4. 全局变量 / 意外挂载到全局
var声明自动挂载 window- 未声明直接赋值自动全局window 是根对象,永远可达 → 永不回收。
5. 闭包导致的内存泄露
不必要的长生命周期闭包,会让内部引用对象一直存活。
6. Map/Set 使用不当
Map/Set 是强引用,即使 key 对象设为 null,Map 依然持有引用,无法回收。
解决方案:使用 WeakMap、WeakSet,它们是弱引用,key 对象被回收时会自动移除对应项,不会造成泄漏。
7. 订阅发布者模式
只订阅不取消订阅,回调函数长期引用外部变量,造成泄漏。
8. Promise 一直不结束
Promise 永久 pending,内部持有大量引用无法释放。
9. 请求未中止,组件已卸载
请求发送后,组件提前卸载,回调仍持有引用。解决方案:使用 AbortController 中止请求。
六、第五部分:栈堆模型对实际开发的深层影响
栈堆不只是面试题,它直接决定了你代码的行为、性能和 Bug 来源。
1. 影响浅拷贝与深拷贝
- 基本类型赋值:拷贝栈值,相互独立
- 引用类型赋值:拷贝地址,共享堆数据
浅拷贝只复制一层地址,深拷贝重新开辟堆内存,完整复制所有结构。理解栈堆,你就彻底理解深浅拷贝的本质区别。
2. 影响闭包原理
闭包的核心就是:栈上的执行上下文销毁了,但变量被堆中的函数引用,因此保留在内存中。
3. 影响内存泄漏
绝大多数泄漏,本质都是:堆对象被意外长期引用,GC 无法回收。
4. 影响页面性能
- 栈内存几乎无性能压力
- 堆内存过多、频繁 GC → 主线程阻塞 → 页面卡顿
七、第六部分:面试满分回答模板(可直接背诵)
如果面试官问:说说 JS 中的栈和堆?
你可以这样回答:
在 JavaScript 中,内存分为栈内存和堆内存。栈主要存储执行上下文、基本类型值、以及引用类型的地址,由系统自动管理,内存连续、后进先出、速度极快,函数执行完自动释放。堆存储对象、数组、函数等复杂数据,内存不连续,需要垃圾回收机制管理,速度相对较慢。
变量存在栈中,对象真实数据存在堆中,栈保存堆的引用地址。垃圾回收主要通过标记清除算法,判断对象是否可达来决定是否回收。
内存泄漏通常是因为对象被意外长期引用,比如未清理定时器、事件监听、DOM 引用、循环引用、不当闭包、强引用 Map 等。实际开发中可以通过及时清理监听、使用 WeakMap、避免冗余闭包来避免泄漏。
这一段覆盖所有要点,逻辑清晰,面试官直接给高分。
八、总结
栈和堆核心区别在于存储内容和管理方式。
- 栈:执行上下文、基本类型、引用地址,系统自动管理,连续内存、LIFO、速度快、无碎片。
- 堆:对象、数组、函数真实数据,GC 管理,非连续、速度较慢、有碎片。
在 JS 中:变量本身在栈中,对象实际数据在堆中,栈存堆地址。 栈随函数结束自动释放;堆只有无引用时才被 GC 回收。
栈堆模型直接影响:深浅拷贝、闭包行为、垃圾回收、内存泄漏、页面性能。
理解栈堆,才算真正理解 JavaScript 的运行底层。