深入理解 JavaScript 内存机制与执行过程
JavaScript 的执行机制、内存模型、闭包形成原理等,都依赖于两个核心组件:调用栈(call stack)与堆内存(heap) 。理解它们如何协作,是掌握 JS 运行机制的关键。
本文结合示意图,系统梳理 JavaScript 的执行过程、内存存储方式及闭包原理。
一、JavaScript 执行机制概览
JS 在执行代码时依赖两个核心结构:
- 调用栈(call stack)
- 执行上下文(Execution Context)
执行上下文包含:
- 变量环境(Variable Environment)
- 词法环境(Lexical Environment)
- outer 环境引用(作用域链)
- this 绑定
下面通过图示理解它们之间的关系。
二、执行上下文与调用栈
当执行一个函数时,JS 会为该函数创建一个新的执行上下文,压入调用栈顶部。
如下图所示(编译阶段生成变量环境与词法环境):
图中可以看到:
- 简单类型(如字符串)直接存储在变量环境中。
- 复杂数据类型只存放地址,实际对象保存在堆内存。
1. 调用栈 push 与 pop 过程
当调用 foo() 时,调用栈结构如下:
执行流程:
- JS 先创建全局执行上下文,压入栈底。
- 调用
foo()时,创建foo 执行上下文压入栈顶。 foo执行结束后,从栈顶弹出(回收),控制权返回全局上下文。
这就是 JavaScript 单线程执行模型的本质。
三、栈内存与堆内存
JavaScript 中的数据类型分成两类:
- 简单数据类型(值类型) :number、string、boolean、null、undefined、symbol、bigint
→ 数据直接存储在栈内存中 - 复杂数据类型(引用类型) :object、array、function
→ 变量存储在栈中,但真正的数据保存在堆内存中,通过地址引用
如下图所示:
图中:
c在栈中保存的是10031003指向堆内存中对象{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; // 错误
类型在编译前就确定,不能随意改变。
七、内存机制如何帮助理解闭包?
闭包之所以能“记住”外部变量,是因为:
- 函数在编译阶段扫描到内部函数 → 发现自由变量 → 创建 closure 对象
- 将这些外部变量迁移到堆中,由内部函数引用
- 即使外层执行上下文被回收,堆中的变量依然存在
示例:
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()都需要访问myName、test1- JS 引擎发现这些自由变量,会将它们提升到堆中
- 即使
foo()执行完被回收,closure 里的变量仍然不会释放
闭包依赖于执行上下文、词法环境与堆内存之间的协作机制。
八、总结
JavaScript 内存机制可归纳为:
-
栈内存
- 存储简单类型及执行上下文
- 快速分配、快速回收
-
堆内存
- 存储复杂类型
- 分配慢但容量大
-
引用类型是通过地址关联堆内存对象
-
闭包的本质
- 将自由变量保存到堆中
- 内部函数继续引用它们,从而延长其生命周期
理解了调用栈、执行上下文、栈和堆,你就理解了 JS 运行的核心。