深入理解 JavaScript 内存机制与执行过程

52 阅读4分钟

深入理解 JavaScript 内存机制与执行过程

JavaScript 的执行机制、内存模型、闭包形成原理等,都依赖于两个核心组件:调用栈(call stack)堆内存(heap) 。理解它们如何协作,是掌握 JS 运行机制的关键。

本文结合示意图,系统梳理 JavaScript 的执行过程、内存存储方式及闭包原理。


一、JavaScript 执行机制概览

JS 在执行代码时依赖两个核心结构:

  • 调用栈(call stack)
  • 执行上下文(Execution Context)

执行上下文包含:

  • 变量环境(Variable Environment)
  • 词法环境(Lexical Environment)
  • outer 环境引用(作用域链)
  • this 绑定

下面通过图示理解它们之间的关系。


二、执行上下文与调用栈

当执行一个函数时,JS 会为该函数创建一个新的执行上下文,压入调用栈顶部。

如下图所示(编译阶段生成变量环境与词法环境):

图中可以看到: 3.png

  • 简单类型(如字符串)直接存储在变量环境中。
  • 复杂数据类型只存放地址,实际对象保存在堆内存。

1. 调用栈 push 与 pop 过程

当调用 foo() 时,调用栈结构如下:

调用栈.png 执行流程:

  • JS 先创建全局执行上下文,压入栈底。
  • 调用 foo() 时,创建 foo 执行上下文 压入栈顶。
  • foo 执行结束后,从栈顶弹出(回收),控制权返回全局上下文。

这就是 JavaScript 单线程执行模型的本质。


三、栈内存与堆内存

JavaScript 中的数据类型分成两类:

  • 简单数据类型(值类型) :number、string、boolean、null、undefined、symbol、bigint
    → 数据直接存储在栈内存
  • 复杂数据类型(引用类型) :object、array、function
    → 变量存储在栈中,但真正的数据保存在堆内存中,通过地址引用

如下图所示:

3.png

图中:

  • c 在栈中保存的是 1003
  • 1003 指向堆内存中对象 {name: "极客时间"}

四、为什么使用栈和堆两种内存?

原因来自存储效率与访问速度之间的平衡:

栈的特点

  • 连续内存空间,访问速度快
  • 存储简单数据类型
  • 管理方式简单,通过栈顶指针移动即可回收

执行上下文的切换,就是栈顶指针的切换,所以栈必须小而快。

堆的特点

  • 用于存储大对象
  • 空间大但分配速度慢
  • 地址不连续,回收靠垃圾回收机制

如果对象也放入栈中,会导致栈空间膨胀、切换缓慢,影响性能,因此引用类型使用堆存储。


五、赋值机制对比:值拷贝 vs 引用拷贝

1. 简单类型:值拷贝

function foo() {
  var a = 1;
  var b = a;
  a = 2;
  console.log(a, b); // 2 1
}
foo();

b = a 时,复制的是值 1,互不影响。


2. 引用类型:引用拷贝

function foo() {
  var a = { name: 'zp' };
  var b = a;
  a.name = 'zp2';
  console.log(a, b); // {name: 'zp2'} {name: 'zp2'}
}
foo();

b = a 时,复制的是地址,两个变量指向同一个对象,因此修改 a.name 会影响 b。


六、JS 是动态弱类型语言

变量在运行过程中可以存储任何类型的值:

var bar;
console.log(typeof bar); // undefined

bar = 11;
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  (历史遗留 Bug)

bar = undefined;
console.log(typeof bar); // undefined

bar = {name: '海澜之家'};
console.log(typeof bar); // object

动态弱类型语言允许变量类型不断变化,JS 在运行期进行类型判断。

对比 C 语言(强类型静态语言):

int a = 1;
char* b = "hello";
bool c = true;
c = a;   // 错误
a = true; // 错误

类型在编译前就确定,不能随意改变。


七、内存机制如何帮助理解闭包?

闭包之所以能“记住”外部变量,是因为:

  1. 函数在编译阶段扫描到内部函数 → 发现自由变量 → 创建 closure 对象
  2. 将这些外部变量迁移到堆中,由内部函数引用
  3. 即使外层执行上下文被回收,堆中的变量依然存在

示例:

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("极客邦")
console.log(bar.getName())

关键点:

  • innerBar.getName()innerBar.setName() 都需要访问 myNametest1
  • JS 引擎发现这些自由变量,会将它们提升到堆中
  • 即使 foo() 执行完被回收,closure 里的变量仍然不会释放 4.png

闭包依赖于执行上下文、词法环境与堆内存之间的协作机制。


八、总结

JavaScript 内存机制可归纳为:

  1. 栈内存

    • 存储简单类型及执行上下文
    • 快速分配、快速回收
  2. 堆内存

    • 存储复杂类型
    • 分配慢但容量大
  3. 引用类型是通过地址关联堆内存对象

  4. 闭包的本质

    • 将自由变量保存到堆中
    • 内部函数继续引用它们,从而延长其生命周期

理解了调用栈、执行上下文、栈和堆,你就理解了 JS 运行的核心。