引言:为什么你需要深入理解 JS 的内存机制?
JavaScript 表面上看是一门“简单”的语言——变量无需声明类型、函数可以嵌套、对象灵活多变。但正是这种“自由”背后,隐藏着一套精密的内存管理机制和作用域系统。如果你只停留在“能跑就行”的层面,迟早会在性能优化、内存泄漏排查、闭包陷阱等问题上栽跟头。
本文将结合代码示例,并融合《你不知道的 JavaScript(上卷)》的核心思想,系统性地拆解 JS 的内存模型、执行上下文、调用栈、堆栈分工、垃圾回收机制以及闭包的本质。我们将用生动的比喻 + 严谨的技术细节 + 实际代码演示,带你彻底搞懂这些概念。
第一部分:JavaScript 是什么语言?—— 动态弱类型的本质
从 3.js 看 JS 的动态弱类型特性
// 3.js
var bar;
console.log(typeof bar); // "undefined"
bar = 12; // number
console.log(typeof bar); // "number"
bar = "极客时间"; // string
console.log(typeof bar); // "string"
bar = true; // boolean
console.log(typeof bar); // "boolean"
bar = null; // 特殊值(历史 bug:typeof null === "object")
console.log(typeof bar); // "object"
bar = {name:"极客时间"}; // object
console.log(typeof bar); // "object"
这段代码完美展示了 JS 的两大特性:
- 动态类型(Dynamic Typing) :变量的类型在运行时确定,而非编译时。
- 弱类型(Weak Typing) :允许隐式类型转换(如
"1" + 2 → "12")。
对比 C 语言(静态强类型)
看 4.c:
int main() {
int a = 10; // 必须声明为 int
char* b = "hello"; // 必须声明为 char*
bool c = true; // C99 后支持 bool
return 0;
}
C 语言要求:
- 编译前必须明确每个变量的类型;
- 类型一旦确定,不能改变;
- 手动管理内存(malloc/free)。
而 JS 完全相反:类型灵活、自动内存管理。这是 Web 开发高效性的基础,但也带来了“不确定性”。
关键结论:JS 的动态弱类型设计,决定了它必须依赖运行时引擎(如 V8) 来动态推断类型、分配内存、回收垃圾。
第二部分:JS 内存模型 —— 栈与堆的分工协作
JS 的内存主要分为两大部分:栈内存(Stack) 和 堆内存(Heap) 。它们各司其职,共同支撑程序运行。
1. 栈内存(Stack):快速、有序、临时
-
用途:存放基本数据类型(Primitive Types)和函数调用信息。
-
基本类型包括(共 7 种):
numberstringbooleanundefinednullsymbol(ES6)bigint(ES2020)
注意:虽然
typeof null === "object",但null仍属于基本类型,存储在栈中。
-
特点:
- 内存连续、分配/释放极快;
- 大小固定(通常几 MB);
- 遵循 LIFO(后进先出)原则;
- 由调用栈(Call Stack) 管理。
示例:1.js —— 值拷贝
// 1.js
function foo(){
var a = 1; // a 存在栈中
var b = a; // b 拷贝 a 的值(不是引用!)
a = 2;
console.log(a); // 2
console.log(b); // 1 → 不受影响
}
foo();
这里,a 和 b 是两个独立的栈变量。修改 a 不会影响 b,因为它们是值拷贝。

2. 堆内存(Heap):大容量、无序、持久
-
用途:存放复杂数据类型(引用类型) ,如:
ObjectArrayFunctionDate、RegExp等
-
特点:
- 内存不连续,分配较慢;
- 容量大(受限于系统内存);
- 通过引用(指针) 访问;
- 由垃圾回收器(GC) 管理生命周期。
示例:2.js —— 引用共享
// 2.js
function foo(){
var a = {name: "极客时间"}; // 对象创建在堆中,a 是栈中的引用
var b = a; // b 拷贝的是引用(地址),不是对象本身
a.name = "极客邦";
console.log(a); // {name: "极客邦"}
console.log(b); // {name: "极客邦"} → 共享同一堆对象!
}
foo();
关键点:
a和b都是指向堆中同一对象的“地址卡”;- 修改对象属性,所有引用都会看到变化;
- 这就是引用传递的本质。

重要区分:
- 基本类型:值存储在栈中,赋值 = 值拷贝;
- 引用类型:值存储在堆中,变量 = 栈中的引用(指针)。
完整内存模型:

3. 为什么这样设计?—— 性能与安全的权衡
“V8 引擎需要用栈来维护程序执行期间上下文的状态。如果栈空间太大了,不连续的(对象也放栈中),会影响上下文切换效率。”
- 栈切换要快:函数调用频繁(如递归、事件回调),栈帧必须小且连续;
- 堆适合大对象:对象大小不确定、生命周期长,不适合放栈;
- 避免栈溢出:若把大对象放栈,极易导致
RangeError: Maximum call stack size exceeded。
因此,JS 引擎采用“小数据放栈,大数据放堆”的策略,是最优解。
第三部分:执行上下文与调用栈 —— 程序运行的舞台
每次函数调用,JS 引擎都会创建一个执行上下文(Execution Context) ,并压入调用栈(Call Stack) 。
执行上下文包含什么?
每个执行上下文包含三个核心部分:
-
变量环境(Variable Environment)
- 存放
var声明的变量、函数声明; - 在创建阶段就初始化(hoisting)。
- 存放
-
词法环境(Lexical Environment)
- 存放
let/const、块级作用域变量; - 更现代的作用域实现方式。
- 存放
-
This 绑定
- 当前上下文的
this值。
- 当前上下文的
《你不知道的 JavaScript》强调:作用域是在词法阶段(写代码时)就确定的,不是运行时决定的。这就是“词法作用域(Lexical Scope)”。
调用栈工作流程
以 1.js 为例:
function foo(){ ... }
foo(); // 调用
- 全局上下文入栈;
- 调用
foo()→ 创建foo的执行上下文,压入栈顶; - 执行
foo内部代码; foo执行完毕 → 上下文弹出栈;- 回到全局上下文。

如果递归太深,栈会溢出(Stack Overflow)。
第四部分:闭包(Closure)—— 作用域的“时光胶囊”
闭包是 JS 最强大也最易误解的特性之一。让我们从现象到本质层层剖析。
什么是闭包?
闭包 = 函数 + 其词法作用域的引用
更准确地说:当一个内部函数引用了外部函数的变量,并且这个内部函数在外部函数执行完毕后仍可被访问,就形成了闭包。
闭包的底层实现
“闭包内部函数引用的自由变量 → JS 判断有闭包 → 堆空间中创建 closure(foo)”
这意味着:
- 引擎在编译阶段(函数定义时)扫描内部函数;
- 如果发现内部函数使用了外部变量(自由变量),就不会让这些变量随栈帧销毁;
- 而是将它们提升到堆内存,并创建一个
closure对象; - 内部函数持有一个指向该
closure的隐藏引用([[Scope]])。
经典闭包示例
function createCounter() {
var count = 0; // 外部变量
return function() {
count++; // 引用外部变量 → 形成闭包
console.log(count);
};
}
var counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3
发生了什么?
| 步骤 | 描述 |
|---|---|
| 1 | createCounter() 被调用,创建执行上下文,count = 0 |
| 2 | 返回匿名函数,该函数引用了 count |
| 3 | createCounter 执行完毕,正常情况下栈帧应销毁 |
| 4 | 但因存在闭包,V8 将 count 移至堆,并创建 closure(createCounter) |
| 5 | counter 变量持有该匿名函数,函数内部可访问 closure 中的 count |
| 6 | 每次调用 counter(),都操作同一个 count |
闭包的本质:延长了外部变量的生命周期,使其不随函数返回而销毁。
例如:
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("极客邦")
bar.getName()
console.log(bar.getName())

闭包的三大用途
-
封装私有变量
var module = (function() { var privateVar = "secret"; return { getSecret: () => privateVar }; })(); console.log(module.getSecret()); // "secret" // 外部无法直接访问 privateVar -
回调函数保持状态
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // 输出 3,3,3(经典问题) } // 用闭包修复: for (var i = 0; i < 3; i++) { (function(j) { setTimeout(() => console.log(j), 100); })(i); // 输出 0,1,2 } -
函数工厂
function makeMultiplier(x) { return function(y) { return x * y; }; } var double = makeMultiplier(2); console.log(double(5)); // 10
闭包的陷阱:内存泄漏
如果闭包引用了大型对象,且长时间不释放,会导致内存无法回收。
function attachListeners() {
var largeData = new Array(1000000).fill("data");
document.getElementById("btn").onclick = function() {
console.log("clicked");
// 即使没用 largeData,闭包仍持有引用!
};
}
解决方案:
- 不需要时,手动置空引用:
largeData = null; - 使用
let/const替代var,缩小作用域; - 避免不必要的闭包。
第五部分:垃圾回收(GC)—— 内存的“清洁工”
JS 不需要手动释放内存,靠自动垃圾回收。
1. 栈内存回收
- 函数执行完毕 → 整个栈帧直接丢弃(指针偏移);
- 极快,无开销。
2. 堆内存回收:标记-清除算法(Mark-and-Sweep)
V8 使用的主要 GC 算法:
- 标记阶段:从根对象(如全局对象、当前执行上下文)出发,遍历所有可达对象,标记为“存活”;
- 清除阶段:未被标记的对象视为垃圾,回收其内存。
关键原则:只要还有引用指向对象,就不会被回收。
闭包与 GC 的关系
- 闭包中的变量之所以不被回收,是因为内部函数仍持有对
closure的引用; - 一旦内部函数也被释放(如
fn = null),closure失去引用,下次 GC 时就会被清理。
第六部分:综合案例分析
文件 1.js → 基本类型 & 栈行为
function foo(){
var a = 1;
var b = a; // 值拷贝
a = 2;
console.log(a); // 2
console.log(b); // 1
}
展示:栈内存的独立性、值传递。
文件 2.js → 引用类型 & 堆共享
var a = {name:"极客时间"};
var b = a;
a.name = "极客邦";
console.log(b.name); // "极客邦"
展示:堆对象的共享性、引用传递。
文件 3.js → 动态类型 & 类型转换
var bar = undefined;
bar = 12;
bar = "极客时间";
...
展示:JS 的动态弱类型本质。
文件 4.c → 静态类型对比
int a = 10;
char* b = "hello";
对比:C 需手动管理类型和内存,JS 自动化。
第七部分:最佳实践与建议
- 理解变量存储位置:基本类型(栈) vs 引用类型(堆);
- 慎用闭包:只在必要时使用,避免内存泄漏;
- 及时释放大对象:
obj = null; - 优先使用
const/let:块级作用域更安全,减少意外闭包; - 避免全局变量:它们永远不会被 GC 回收;
- 使用 Chrome DevTools 分析内存:Memory 面板可检测泄漏。
结语:从“会用”到“懂原理”
JavaScript 的内存机制和闭包,不是玄学,而是工程设计的必然结果。V8 引擎通过栈与堆的分工、词法作用域的静态绑定、闭包的堆提升、自动垃圾回收等机制,在开发效率与运行性能之间找到了精妙的平衡。
正如 Kyle Simpson 在《你不知道的 JavaScript》中所说:
“作用域和闭包不是为了让你写出炫技的代码,而是为了让你写出可预测、可维护、可推理的代码。”
当你真正理解了这些底层机制,你写的每一行 JS,都将更有底气。
参考资料
- 《你不知道的 JavaScript(上卷)》— Kyle Simpson
- V8 官方文档:[v8.dev/](v8.dev/ "v8.dev/")
- MDN Web Docs: [Memory Management](developer.mozilla.org/en-US/docs/… "Memory Management")
- 《深入浅出 Node.js》— 朴灵(含 V8 内存模型章节)
- Google Developers: [JavaScript Memory Profiling](developer.chrome.com/docs/devtoo… "JavaScript Memory Profiling")
- 示例代码仓库:lesson_zp: AI + 全栈学习仓库——内存机制