从栈与堆到闭包:深入 JavaScript 内存机制

从栈与堆到闭包:深入 JavaScript 内存机制

JavaScript 作为一门广泛使用的编程语言,其"动态弱类型"的特性为开发者带来了极大的灵活性。然而,这种灵活性背后依赖于一套精密的内存管理与执行机制。本文将结合提供的代码示例和知识文档,系统地解析 JavaScript 的内存结构、执行上下文、闭包机制以及数据类型的存储方式。


一、JavaScript 是动态弱类型语言

我们看一下几行代码:

ini 复制代码
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 的一个历史 bug)

这段代码充分体现了 动态性 (变量类型在运行时可变)和 弱类型(不同类型之间可以隐式转换)。JavaScript 不要求在声明变量时指定类型,且可以在运行过程中改变变量的值和类型,这虽然提高了开发效率,但也对程序员理解底层运行机制提出了更高要求。


二、JavaScript 内存空间的三大"天王"

JavaScript 引擎(如 V8)在运行程序时,会将内存划分为三个逻辑区域,分别承担不同职责:

1. 代码空间(Code Space)

  • 存放 JavaScript 源代码编译后的字节码或机器码
  • 由引擎管理,开发者不可见;
  • 程序启动时,代码从硬盘加载到此区域,供执行引擎调用。

2. 栈内存(Stack)

  • 用于存储执行上下文原始数据类型 (如 numberstringbooleanundefinednullsymbolbigint);
  • 特点:体积小、连续、分配/回收极快
  • 每次函数调用都会在栈中创建一个栈帧(即执行上下文),函数返回后立即释放;
  • 栈顶指针频繁切换,因此必须保持轻量------这也是对象不能放在栈中的原因。

3. 堆内存(Heap)

  • 用于存储复杂数据类型 (引用类型),如 ObjectArrayFunction
  • 特点:空间大、不连续、分配与回收成本高
  • 所有对象都分配在堆中,变量仅保存指向堆中对象的引用(地址)
  • 由垃圾回收器(GC)自动管理,当对象不再被任何变量引用时,才可能被回收。

💡 为什么这样设计?

如果把对象也放进栈中,会导致栈帧过大且大小不固定,严重影响上下文切换效率。而将对象统一放入堆中,栈只需保存小而固定的引用地址,既保证了执行速度,又支持动态数据结构。

示例对比

栈内存中的值拷贝

ini 复制代码
function foo() {
    var a = 1;
    var b = a; // 值拷贝
    a = 2;
    console.log(a, b); // 2 1
}
foo();

ab 是两个独立的栈变量,互不影响。

堆内存中的引用共享

ini 复制代码
function foo() {
    var a = {name: "极客时间"};
    var b = a; // 引用拷贝
    a.name = '极客邦';
    console.log(b); // {name: "极客邦"}
}
foo();

ab 共享同一个堆对象。

核心区别:原始类型按值传递,引用类型按引用传递。

三、执行上下文与调用栈

当 JavaScript 引擎执行代码时,会创建一个执行上下文 (Execution Context),它包含变量环境、词法环境等信息。每个函数调用都会创建一个新的执行上下文,并被压入调用栈(Call Stack)中, 调用栈是 JavaScript 引擎用来管理函数调用顺序的数据结构。

执行 foo 函数时与执行结束后的调用栈变化

如图所示,在调用 foo() 函数时,JavaScript 引擎会在全局执行上下文之上创建一个新的 foo 函数执行上下文,并将其推入调用栈顶部。此时当前执行上下文指向 foo 函数执行上下文。

foo 函数执行完毕后,该执行上下文会被弹出调用栈,当前执行上下文指针重新指向全局执行上下文,而 foo 函数的执行上下文则进入回收阶段,由垃圾回收器处理。


四、变量环境与词法环境

每个执行上下文都包含两个重要组成部分:变量环境 (Variable Environment)和词法环境(Lexical Environment)。前者用于存储函数内部声明的变量和函数,后者则用于实现作用域链机制。

foo 函数执行上下文中的变量环境

如图所示,foo 函数执行上下文的变量环境中包含了四个变量:abcd。其中 ab 被赋值为字符串 "极客时间",而 cd 尚未初始化,因此其值为 undefined。这些变量名与值的映射关系构成了函数内部的数据存储基础。


五、堆空间与引用类型存储

JavaScript 中的数据分为原始类型(如 string、number、boolean)和引用类型(如 object、array、function)。原始类型直接存储在栈中,而引用类型则存储在堆空间中,变量只保存指向堆中对象的地址。

引用类型变量在堆中的存储方式

如图所示,变量 c 的值是 1003,这是一个指向堆空间中某个对象的内存地址。该地址对应的堆内存单元中存储了一个对象 {name: "极客时间"}。这意味着 c 并不直接持有对象内容,而是通过指针引用它。这种设计使得多个变量可以共享同一对象,也带来了诸如浅拷贝、深拷贝等问题。


六、闭包与持久化执行上下文

闭包是 JavaScript 中非常重要的概念,它允许函数访问其外部作用域中的变量,即使外部函数已经执行完毕。关键在于:当一个函数被返回或传递给另一个函数时,它的执行上下文不会立即被销毁,而是被保留下来

闭包中对堆内存的引用保持

如图所示,clourse(foo) 是一个闭包函数,它引用了 test2 变量(值为 2)和 clourse(foo) 自身所引用的地址 1003。尽管 foo 函数已经执行完成,但由于闭包的存在,其执行上下文仍然存在于内存中,从而保证了对堆空间中对象 {myName: "极客时间", test1: 1} 的持续访问能力。


七、总结

JavaScript 的执行过程是一个复杂但有序的过程,涉及调用栈、执行上下文、变量环境、堆与栈的协同工作。理解这些机制有助于我们写出更高效、更稳定的代码,尤其是在处理异步操作、内存泄漏和闭包问题时。

  • 调用栈决定了程序的执行顺序;
  • 执行上下文承载了函数运行所需的所有环境信息;
  • 变量环境管理局部变量;
  • 堆空间存放对象实例;
  • 闭包通过保留执行上下文实现了对外部变量的持久访问。

掌握这些核心概念,是成为高级 JavaScript 开发者的关键一步。

相关推荐
徐小夕2 小时前
知识库创业复盘:从闭源到开源,这3个教训价值百万
前端·javascript·github
xhxxx2 小时前
函数执行完就销毁?那闭包里的变量凭什么活下来!—— 深入 JS 内存模型
前端·javascript·ecmascript 6
L、2182 小时前
统一日志与埋点系统:在 Flutter + OpenHarmony 混合架构中实现全链路可观测性
javascript·华为·智能手机·electron·harmonyos
十一.3663 小时前
103-105 添加删除记录
前端·javascript·html
用户47949283569153 小时前
面试官:DNS 解析过程你能说清吗?DNS 解析全流程深度剖析
前端·后端·面试
陳陈陳3 小时前
闭包、栈堆与类型之谜:JS 内存机制全解密,面试官都惊了!
前端·javascript
UrbanJazzerati3 小时前
Salesforce Summer '25 新特性:TypeScript 支持和本地开发预览
面试
诗和远方14939562327343 小时前
iOS 电量监控与优化完整方案
面试