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

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

作为 JavaScript 开发者,我们享受着自动内存管理的便利。不同于 C 或 C++ 需要使用 mallocfree 手动操作内存,JS 引擎在幕后默默地为我们处理了一切。然而,理解内存机制(尤其是栈和堆)对于编写高性能、无 Bug 的代码至关重要,特别是当你面对复杂的对象引用和闭包时。

本文将结合 V8 引擎的机制,带你彻底搞懂 JS 的内存管理。

1. JavaScript:动态弱类型语言

在深入内存之前,我们需要先认清 JavaScript 的语言特性:它是动态弱类型的语言。

  • 动态语言:在运行过程中才检查变量的数据类型,使用前无需确认。
  • 弱类型:支持隐式类型转换。

相比之下,像 C 语言这样的静态语言在编译时就必须确定类型(如 int a = 1)。而在 JavaScript 中,同一个变量可以随意改变其"形状":

JavaScript

javascript 复制代码
// 代码来源:3.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 (这是 JS 设计的一个知名 Bug)

这种灵活性意味着内存分配必须能够适应数据的动态变化。

2. 内存模型:栈 (Stack) vs 堆 (Heap)

JavaScript 的内存主要分为两块区域:栈内存堆内存

栈内存 (Stack Memory)

栈内存主要用于存储调用栈 (JS 执行的主角)和简单数据类型(Primitive Types)。

  • 特点:空间小,内存连续,分配和回收速度极快。
  • 赋值行为 :赋值是值拷贝

JavaScript

ini 复制代码
// 代码来源:1.js
function foo() {
    var a = 1;   
    var b = a;   // 拷贝:b 拥有了独立的值 1
    a = 2;       // 修改 a 不会影响 b
    console.log(a);   // 2
    console.log(b);   // 1
}
foo();

因为 ab 是简单类型,它们直接存储在栈中,互不干扰。

堆内存 (Heap Memory)

堆内存用于存储复杂数据类型(如 Object, Array, Function)。

  • 特点:空间大,数据不连续,分配和回收较耗时。
  • 赋值行为 :栈中存储的是引用地址 ,实际对象存储在堆中。赋值是引用拷贝

JavaScript

css 复制代码
// 代码来源:2.js
function foo() {
    var a = {name: "极客时间"}  // 栈中存地址,堆中存对象
    var b = a;                // 引用拷贝:b 拿到的是同一个地址
    a.name = "极客邦";         // 通过 a 修改堆中的对象
    
    console.log(a);   // {name: "极客邦"}
    console.log(b);   // {name: "极客邦"} -> b 也随之改变
}
foo();

在这里,ab 在栈上是两个不同的变量,但它们指向堆中同一个内存地址。

为什么要区分栈和堆?

为什么不把所有数据都放在栈里?

V8 引擎使用栈来维护程序的执行上下文。上下文切换本质上是栈顶指针的偏移。

如果栈空间太大或存储了不连续的大型数据,会严重影响栈顶切换的效率,进而拖慢整个程序的执行速度。

  • :负责状态维护和执行,要求"快、小、连续"。
  • :负责存储大数据,"打辅助"。

3. 内存视角下的闭包 (Closure)

闭包是 JS 中最迷人也最容易导致内存泄漏的特性。从内存角度看,闭包是如何工作的?

通常情况下,函数执行完毕,其栈内的局部变量会被销毁。但闭包允许变量"逃逸"出来。

V8 的处理流程:

  1. 预扫描 :在编译 foo 函数时,引擎会进行快速的词法扫描。
  2. 发现闭包:如果发现内部函数引用了外部变量(自由变量),引擎会判断这是一个闭包。
  3. 堆分配 :引擎会在堆空间 中创建一个 closure(foo) 对象,将这些被引用的变量(如 myName, test1)保存进去,而不是让它们留在随时可能被销毁的栈中。

JavaScript

javascript 复制代码
// 代码来源:6.html
function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    
    // innerBar 引用了 myName 和 test1
    var innerBar = { 
        setName: function(newName){
            myName = newName
        },
        getName: function(){
            console.log(test1)
            return myName
        }
    }
    return innerBar
}
var bar = foo()

在执行 foo() 时:

  • 虽然 foo 执行结束了,但 myNametest1 已经被移动到了堆内存的闭包对象中。
  • bar.getName() 执行时,它访问的是堆中的 closure(foo),而不是栈上的数据。

总结

特性 栈内存 (Stack) 堆内存 (Heap)
存储数据 简单数据类型 (Number, String等) 复杂数据类型 (Object等)
存储方式 直接存储值 栈存地址,堆存实体
效率 极快(指针偏移) 较慢(动态分配)
闭包变量 无法长期保存 闭包引用的变量会迁徙至此

理解这套机制,你就能明白为什么修改对象属性会影响其他变量,以及闭包是如何神奇地"锁住"变量的。

相关推荐
search71 小时前
前端设计:CRG 3--CDC error
前端
治金的blog2 小时前
vben-admin和vite,ant-design-vue的结合的联系
前端·vscode
利刃大大3 小时前
【Vue】Vue2 和 Vue3 的区别
前端·javascript·vue.js
Lhuu(重开版3 小时前
JS:正则表达式和作用域
开发语言·javascript·正则表达式
荔枝一杯酸牛奶4 小时前
HTML 表单与表格布局实战:两个经典作业案例详解
前端·html
Charlie_lll4 小时前
学习Three.js–纹理贴图(Texture)
前端·three.js
yuguo.im4 小时前
我开源了一个 GrapesJS 插件
前端·javascript·开源·grapesjs
安且惜4 小时前
带弹窗的页面--以表格形式展示
前端·javascript·vue.js
摘星编程6 小时前
用React Native开发OpenHarmony应用:NFC读取标签数据
javascript·react native·react.js
GISer_Jing6 小时前
AI驱动营销:业务技术栈实战(From AIGC,待总结)
前端·人工智能·aigc·reactjs