JavaScript 内存机制与闭包:从栈到堆的深入浅出
在前端开发中,JavaScript 是我们最常用的编程语言之一。然而,很多人在学习 JS 的过程中,常常忽略了它背后运行的底层机制——内存管理。今天我们就结合几段代码和图示,来聊聊 JavaScript 中最重要的两个概念:内存机制 和 闭包。
一、JS 的执行环境:三大内存空间
在 JavaScript 执行过程中,程序会使用三种主要的内存空间:
- 代码空间
- 栈内存(Stack)
- 堆内存(Heap)
![]()
图1:JavaScript 的三大内存空间
1. 代码空间
这是存放源代码的地方。当浏览器加载 HTML 文件时,会把 <script> 标签中的代码从硬盘读取到内存中,形成“代码空间”。这部分内容不会直接参与运行,而是供引擎解析和编译用。
2. 栈内存
栈内存是 JS 执行的主角,用于维护函数调用过程中的执行上下文。它的特点是:
- 空间小、速度快
- 连续存储,便于快速切换
- 每次函数调用都会创建一个执行上下文并压入栈顶
function foo() {
var a = 1;
var b = a;
a = 2;
console.log(a); // 2
console.log(b); // 1
}
foo();
这段代码执行时,foo() 被调用,会生成一个新的执行上下文,并压入调用栈。这个上下文包含变量 a 和 b,它们都是简单数据类型,直接存储在栈中。
3. 堆内存
堆内存用来存储复杂数据类型,比如对象、数组等。这些数据体积大、结构复杂,并且是动态的,不能放在栈里,所以被分配到堆中。 它的特点是:
- '辅助栈内存'
- 空间大 不连续
- 存储复杂数据类型 对象
function foo() {
var a = { name: '极客时间' };
var b = a; // 引用拷贝
a.name = '极客邦';
console.log(a); // {name: "极客邦"}
console.log(b); // {name: "极客邦"}
}
foo();
这里 a 是一个对象,它实际存储在堆内存中,而 a 变量只是保存了该对象的地址(引用)。b = a 并不是复制对象,而是让 b 也指向同一个地址。因此修改 a.name 后,b 也能看到变化。
为什么堆内存是不连续的?
- 因为对象是动态的 可以去给它添加属性或方法 如果是连续的情况下 显然就不好进行操作了
二、简单 vs 复杂 数据类型:存储方式不同
JavaScript 有八种原始数据类型:
undefined, null, boolean, string, number, symbol, bigint, object
其中前七种是简单数据类型,最后一个是复杂数据类型。
| 类型 | 存储位置 |
|---|---|
| 简单类型(如 number, string) | 栈内存 |
| 复杂类型(如 object, array) | 堆内存 |
示例说明
var a = '极客时间'; // 字符串 → 栈内存
var b = a; // 拷贝值 → b 也是 '极客时间'
a = '极客邦'; // 修改 a 不影响 b
console.log(a); // 极客邦
console.log(b); // 极客时间
var c = { name: '极客时间' }; // 对象 → 堆内存
var d = c; // 引用拷贝 → d 指向同一地址
c.name = '极客邦'; // 修改共享对象
console.log(c.name); // 极客邦
console.log(d.name); // 极客邦
✅ 结论:
- 简单类型是“值传递”,每个变量独立存在
- 复杂类型是“引用传递”,多个变量可能指向同一块堆内存
![]()
图2:变量
c存储的是堆内存中对象的地址(1003)
三、执行上下文与调用栈
JavaScript 的执行流程依赖于调用栈(Call Stack),它是函数调用的“记录本”。
每当一个函数被执行,就会创建一个执行上下文(Execution Context),包括:
- 变量环境(Variable Environment)
- 词法环境(Lexical Environment)
- outer(外层作用域链)
执行上下文会被压入调用栈顶部。当函数执行完毕后,该上下文就会被弹出并回收。
示例:函数调用前后
function foo() {
var a = 1;
var b = 2;
}
foo(); // 调用 foo
执行过程如下:
- 创建全局执行上下文(已存在)
- 调用
foo(),创建新的执行上下文,压入调用栈 - 执行完
foo(),将其执行上下文弹出,指针回到全局上下文
![]()
图3:
foo()执行前后调用栈的变化
注意:栈顶指针移动非常快,因为栈是连续内存,只需要改变指针位置即可完成上下文切换(栈顶指针的切换通过一个机制,做内存的减法)。如果将复杂对象也放在栈中,会导致栈空间过大、不连续,严重影响性能。
四、动态弱类型语言:JS 的灵活性
JavaScript 是一种动态弱类型语言,这意味着:
- 动态:变量可以在运行时改变类型
- 弱类型:不同类型之间可以自动转换
var bar;
console.log(typeof bar); // undefined
bar = 12;
console.log(typeof bar); // number
bar = '极客时间';
console.log(typeof bar); // string
bar = true;
console.log(typeof bar); // boolean
bar = null;
console.log(typeof bar); // object(JS 设计缺陷)
bar = { name: '极客时间' };
console.log(typeof bar); // object
这正是 JS 的魅力所在:无需提前声明类型,灵活自由。但这也带来了潜在问题,比如 typeof null === 'object' 就是一个经典 bug。
五、闭包的本质:延长变量生命周期
现在我们来看一个关键概念——闭包。
什么是闭包?
闭包是指:内部函数能够访问外部函数的变量,即使外部函数已经执行完毕。
<script>
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("极客邦");
bar.getName(); // 输出:1, 极客邦
</script>
在这个例子中,innerBar 返回了一个对象,其方法 setName 和 getName 都能访问 myName 和 test1。但 foo() 已经执行完了,按理说这些变量应该被销毁了,为什么还能访问?
这就是闭包的作用!
六、闭包背后的内存机制
闭包是如何工作的?答案就在堆内存中。
步骤解析:
- 当
foo()被调用时,V8 引擎会扫描内部函数setName和getName - 发现这两个函数引用了外部变量
myName和test1 - 引擎判断:这是一个闭包!需要保留这些变量
- 在堆内存中创建一个特殊的对象:
closure(foo) - 把
myName和test1的值保存到这个对象中 - 将
closure(foo)的地址存入栈中(作为innerBar的一部分)
![]()
图4:闭包
closure(foo)存储在堆内存中,栈中只保存地址
关键点总结:
- 闭包本质:通过堆内存延长了外部变量的生命周期
- 栈中保存的是地址,堆中保存的是真实数据
- 执行上下文出栈 ≠ 变量消失,只要还有引用,就继续存活
七、为什么 JS 不需要手动管理内存?
像 C/C++ 这样的语言,开发者必须使用 malloc 和 free 来手动分配和释放内存:
int main(){
int a = 1;
char* b = '极客时间';
bool c = true;
c = a;
return 0;
}
而在 JavaScript 中,你完全不需要关心这些。V8 引擎自动完成内存分配和垃圾回收。
- 栈内存:执行上下文结束 → 自动弹出 → 快速回收
- 堆内存:没有变量引用的对象 → 垃圾回收器(GC)定期清理
✅ 所以 JS 开发者可以专注于业务逻辑,而不必担心内存泄漏(虽然也要注意,比如事件监听器未解绑可能导致内存泄漏)
八、总结:JS 内存机制核心要点
| 内容 | 说明 |
|---|---|
| 栈内存 | 调用栈在其内部,存放执行上下文、简单数据类型,速度快,连续 |
| 堆内存 | 存放复杂对象,空间大,不连续 |
| 变量提升 | 编译阶段为变量预留空间,初始值为 undefined
|
| 引用传递 | 对象赋值是地址拷贝,多个变量共享同一对象 |
| 闭包 | 内部函数引用外部变量 → 堆内存保存自由变量 → 延长生命周期 |
| 栈顶指针 | 函数调用时移动,上下文切换高效 |
九、写在最后
理解 JavaScript 的内存机制,不仅能帮助我们写出更高效的代码,还能更好地掌握闭包、作用域、垃圾回收等高级概念。虽然我们不需要像 C++ 那样手动操作内存,但了解背后的原理,会让你的代码更加稳健。
💡 提醒:不要滥用闭包,因为它会让变量长期驻留内存,增加 GC 压力。合理使用,才是高手之道。
📌 如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!关注我,带你深入浅出地学透前端技术!
📸 图片来源:本文所有图片均为原创示意图,用于辅助理解 JS 内存模型。