深入理解 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等)
存储方式 直接存储值 栈存地址,堆存实体
效率 极快(指针偏移) 较慢(动态分配)
闭包变量 无法长期保存 闭包引用的变量会迁徙至此

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

相关推荐
Justin3go8 小时前
HUNT0 上线了——尽早发布,尽早发现
前端·后端·程序员
怕浪猫8 小时前
第一章 JSX 增强特性与函数组件入门
前端·javascript·react.js
铅笔侠_小龙虾9 小时前
Emmet 常用用法指南
前端·vue
钦拆大仁9 小时前
跨站脚本攻击XSS
前端·xss
前端小L9 小时前
贪心算法专题(十):维度权衡的艺术——「根据身高重建队列」
javascript·算法·贪心算法
VX:Fegn089510 小时前
计算机毕业设计|基于springboot + vue校园社团管理系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·课程设计
Fortunate Chen10 小时前
类与对象(下)
java·javascript·jvm
ChangYan.11 小时前
直接下载源码但是执行npm run compile后报错
前端·npm·node.js
skywalk816311 小时前
在 FreeBSD 上可以使用的虚拟主机(Web‑Hosting)面板
前端·主机·webmin