从调用栈到闭包,从原始类型到堆内存,用最顺的比喻、最狠的例子,让你一次性看懂 JS 的底层逻辑。
看完:你写代码会更稳,你 debug 会更快,你面试官会更爱你。
🌈 1. JS 的世界观:每段代码都是一场“搬运工协作大会”
当你在写 JS 时,其实你在指挥三位主角:
- 执行上下文(Execution Context) :负责“场景搭建”
- 调用栈(Call Stack) :负责“流程调度”
- 内存系统(Memory System) :负责“存储与回收”
如果把 JavaScript 比作一栋大楼:
- 栈内存(Stack) 是电梯井:
👉 空间固定、连续、速度超快。 - 堆内存(Heap) 是自由办公室:
👉 空间巨大、布局混乱、对象随意入住。 - 代码区(Code Area) 则是大楼自带的书架:
👉 存放已加载的 JS 字节码。
理解这三个区,你就会明白:“原始类型为什么放栈里,对象为什么放堆里,闭包为什么能记住外部变量。”
🎯 2. 核心问题:JS 为什么要分成“栈”和“堆”?
🧱 理由很简单:为了快。超级快。快得飞起。
🔹 栈内存特点
- 连续空间(像一摞叠好的书)
- 分配回收都依赖 指针移动
- 创建变量:压栈
- 执行完毕:指针往回移,整层一起“消失”
适合存:
- number、string、boolean、undefined、null、symbol、bigint
👉 体积小、不变、生命周期短
🔹 堆内存特点
- 空间大且不连续(像办公室租赁格子)
- 分配慢、回收更慢
- 适合存:
👉 对象、数组、函数等复杂结构
为什么对象要放堆?
因为对象大小不确定(属性随时加),无法放在栈这种连续空间中。
🌟 一句话总结:
简单类型 → 一次性存,生命周期短
复杂类型 → 可能越长越大,必须堆里养着
🧪 3. 栈 vs 堆:50 行代码看穿所有细节
🍬 简单类型:复制的是“值”
function foo(){
var a = 1 // 栈里开辟一格:{a:1}
var b = a // 复制值,b 的格子里也是 1
a = 2
console.log(a) // 2
console.log(b) // 1
}
📌 这里没有任何对象,所有数据都“放在栈里”,修改 a 不会影响 b。
🍱 复杂类型:复制的是“地址”
function foo(){
var a = {name:"极客时间"} // a 存的是堆对象的地址
var b = a // b 存的是同一个地址
a.name = "极客邦"
console.log(a) // {name: "极客邦"}
console.log(b) // {name: "极客邦"}
}
📌 两个变量依然指向同一个堆对象,这是 JS 引用类型最关键的机制。
🌀 4. 执行上下文:JS 如何知道变量在哪?
每次函数执行时,都会生成一个执行上下文(Execution Context):
包含三个部分:
- 变量环境(Variable Environment) ——处理
var - 词法环境(Lexical Environment) ——处理
let、const - outer 指针(Scope Chain) ——作用域链
还有一个特别重要的:
- this 绑定
JS 用这一整套东西去构成执行栈:
函数每调用一次,就往栈顶 push 一层,执行完就 pop。
🔥 5. 闭包:你以为是语法糖,其实是堆内存黑魔法
闭包为什么能保持外部变量?
一句话:
JS 在编译阶段扫描到内部函数引用外部变量时,会把这些变量“搬”进堆里,形成一个 closure 对象。
看例子:
function foo() {
var myName = "极客时间"
let test1 = 1
const test2 = 2
var innerBar = {
setName:function(newName){
myName = newName
},
getName:function(){
console.log(test1)
return myName
}
}
return innerBar
}
var bar = foo()
bar.setName("极客邦")
console.log(bar.getName())
流程解析:
🥇 步骤 1:编译阶段扫描内部函数引用了哪些外部变量
扫描到:
- myName
- test1
- test2
JS 判断需要闭包,于是把这 3 个变量放入堆内存:
Heap:
closure(foo) = {
myName: "极客时间",
test1: 1,
test2: 2
}
🥈 步骤 2:返回的函数绑定这个闭包地址
bar.getName() 和 bar.setName() 都指向这同一个 closure。
所以函数执行完 foo 也不会被回收。
🥉 步骤 3:外层函数栈被销毁,但堆里的 closure 继续存在
这就是闭包不释放的本质。
⚠️ 6. JS 是动态弱类型:变量不是固定类型的
var bar
console.log(typeof bar) // undefined
bar = 12
console.log(typeof bar) // number
bar = "极客时间"
console.log(typeof bar) // string
bar = null
console.log(typeof bar) // object(历史遗留 Bug)
这就是 JS 的核心:
- 变量没有类型
- 值才有类型
- 变量随时可以指向不同类型的值
也因此:
JS 完全不需要像 C/C++ 一样手动 malloc/free。
♻️ 7. JS 的垃圾回收:栈快、堆慢
✔ 栈内存回收
执行上下文一结束:
👉 “整层数据”指针一移,瞬间消失。
回收速度极快。
✔ 堆内存回收
由 V8 的垃圾回收器:
- 新生代(Scavenge)
- 老生代(Mark-Sweep、Mark-Compact)
对象没有引用时:
👉 标记为可回收
👉 空闲时才真正释放
闭包容易造成内存泄漏,就是因为对象一直被引用着。
🧭 8. 所有知识串成一句话(面试必杀)
JS 把轻巧的原始值放栈里,把会长大的对象放堆里;闭包把外部变量从栈里搬到堆里;垃圾回收器按引用关系清理堆;执行栈用来管理所有函数的上下文切换。
学完这个,你对 JS 的理解已经超过 90% 的前端开发者。
🎁 9. 完整总结
🌟 JS 内存体系的三大区
- 代码区:存放字节码
- 栈内存:原始类型 + 执行上下文
- 堆内存:对象、函数、闭包
🌟 栈快、堆大,各司其职
- 栈:连续、高速、自动回收
- 堆:灵活、可变、慢一点但能放大物体
🌟 闭包是堆内存黑魔法
- 内部函数引用外部变量
- 扫描绑定 → 放入 closure
- 函数返回后上下文消失,closure 继续存活
🌟 动态弱类型
- JS 变量不固定类型
- 变量只是“指向值的引用”
🚀 10. 彩蛋:一张图总结全篇
🏁 结语:理解内存,你才算真正懂 JS
当你不再把 JS 看成“跑逻辑的工具”,
而是看成“管理内存的大型系统”,
你会发现:
- 变量为何这样变?
- 闭包为何能记住外部变量?
- 为什么对象传来传去都会被改?
- 为什么浏览器页面越用越卡?
全都通了。