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

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

相关推荐
尘中客3 小时前
放弃 Echarts?前端直接渲染后端高精度 SVG 矢量图流的踩坑记录
前端·javascript·echarts·前端开发·svg矢量图·echarts避坑
FreeBuf_4 小时前
Chrome 0Day漏洞遭野外利用
前端·chrome
小彭努力中4 小时前
199.Vue3 + OpenLayers 实现:点击 / 拖动地图播放音频
前端·vue.js·音视频·openlayers·animate
2501_916007474 小时前
网站爬虫原理,基于浏览器点击行为还原可接口请求
前端·javascript·爬虫·ios·小程序·uni-app·iphone
前端大波4 小时前
Sentry 每日错误巡检自动化:设计思路与上手实战
前端·自动化·sentry
Highcharts.js5 小时前
适合报表系统的可视化图表|Highcharts支持直接导出PNG和PDF
javascript·数据库·react.js·pdf
ZC跨境爬虫5 小时前
使用Claude Code开发校园交友平台前端UI全记录(含架构、坑点、登录逻辑及算法)
前端·ui·架构
慧一居士5 小时前
Vue项目中,何时使用布局、子组件嵌套、插槽 对应的使用场景,和完整的使用示例
前端·vue.js
叫我一声阿雷吧5 小时前
JS 入门通关手册(35):执行上下文、调用栈与作用域链深度解析
javascript·作用域链·js进阶·执行上下文·调用栈·变量提升·闭包原理
Можно5 小时前
uni.request 和 axios 的区别?前端请求库全面对比
前端·uni-app