🌟 本文将带你从零开始,深入理解 JavaScript 的内存机制、执行上下文、词法环境、闭包等核心概念。无论你是刚接触前端的新手,还是想夯实底层原理的进阶开发者,这篇文章都为你量身打造!
第一部分:JavaScript 是什么类型的语言?🧠
在深入内存之前,我们先要搞清楚 JavaScript 的语言特性。
✅ 动态弱类型语言
JavaScript 是一门 动态弱类型语言,这意味着:
- 动态:变量的数据类型是在运行时确定的,而不是在声明时。
- 弱类型:不同数据类型之间可以自动转换(隐式类型转换)。
来看一个例子:
let bar;
console.log(typeof bar); // "undefined"
bar = 12;
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" ← 这是 JS 的一个历史 bug!
🔍 对比 C 语言(静态弱类型)
int a = 1;
char* b = "极客时间";
bool c = true;
c = a; // ❌ 编译器会报错或警告(除非强制转换)
在 C 中:
- 变量类型一旦声明就不能随意改变(静态类型特征);
- 但 C 允许大量隐式类型转换(比如
int转float、char转int),属于 弱类型语言。
而在 JS 中:
- 变量可以随时“变身”(动态类型);
- 类型转换更宽松(弱类型)。
💡 关键点:
JS 不需要你提前声明变量类型,引擎会在运行时自动判断和处理;而 C 必须声明类型(静态),但类型约束不如 Java/C# 等强类型语言严格(弱类型)。
❓ 答疑解惑 Q&A
- Q1:为什么
typeof null返回"object"?
这是 JavaScript 创始人 Brendan Eich 在 1995 年设计时的一个历史性 bug。由于底层用 3 位二进制标识类型,null的机器码全为 0,被误判为对象。虽然后来发现,但为了兼容性一直保留至今。 - Q2:动态语言 vs 静态语言,哪个更好?
没有绝对好坏。动态语言开发快、灵活;静态语言更安全、性能高。TypeScript 就是在 JS 基础上加了静态类型检查,兼顾两者优点。
第二部分:JavaScript 的内存模型 🏗️
JavaScript 引擎(如 V8)将内存分为两大区域:栈内存(Stack) 和 堆内存(Heap) 。
📦 栈内存(Stack)—— 存放简单数据 & 执行上下文
-
特点:速度快、空间小、连续分配、自动管理。
-
存储内容:
- 原始类型(Primitive Types):
number,string,boolean,undefined,null,symbol,bigint - 函数调用记录(即“执行上下文”)
- 原始类型(Primitive Types):
示例:值拷贝(简单类型)
function foo() {
var a = 1;
var b = a; // 拷贝值
a = 2;
console.log(a); // 2
console.log(b); // 1 ← 不受影响
}
foo();
这里 a 和 b 是两个独立的栈变量,互不影响。
🗃️ 堆内存(Heap)—— 存放复杂对象
-
特点:空间大、不连续、分配/回收慢、需垃圾回收(GC)。
-
存储内容:
- 对象(Object)、数组(Array)、函数(Function)等引用类型
示例:引用拷贝(复杂类型)
function foo() {
var a = { name: '极客时间' };
var b = a; // 拷贝的是引用地址(指针)
a.name = '极客邦';
console.log(a); // { name: '极客邦' }
console.log(b); // { name: '极客邦' } ← 被同步修改!
}
foo();
a 和 b 都指向堆中同一个对象,所以修改一个会影响另一个。
🔍 内存图示:
栈内存:
a ──┐
├──→ [堆内存中的对象 { name: '极客邦' }]
b ──┘
❓ 答疑解惑 Q&A
- Q1:为什么简单类型放栈,复杂类型放堆?
因为简单类型体积小、生命周期短,适合快速分配/释放;而对象大小不确定、可能很大,放在堆中更灵活,避免栈溢出。 - Q2:JS 能直接操作内存吗?
不能!不像 C/C++ 可以用malloc/free手动管理内存。JS 的内存由引擎自动管理(通过垃圾回收机制),开发者无需关心。
第三部分:执行上下文(Execution Context)与调用栈 🧩
每当 JS 代码运行,引擎都会创建一个 执行上下文(Execution Context) ,它是代码执行的“舞台”。
🎭 执行上下文的组成
每个执行上下文包含两个核心环境:
-
变量环境(Variable Environment)
- 存放
var声明的变量、函数声明 - 在代码执行前就完成初始化(变量提升)
- 存放
-
词法环境(Lexical Environment)
- 存放
let、const、块级作用域变量 - 更精确地反映代码的词法结构(即“写在哪里”)
- 存放
📌 注意:现代 JS 引擎中,这两个环境在逻辑上是分开的,但实际实现可能合并。
📚 调用栈(Call Stack)—— 执行上下文的管理者
- JS 是单线程语言,靠 调用栈 管理函数调用顺序。
- 每调用一个函数,就压入一个执行上下文;函数执行完,就弹出。
示例:
function greet() {
console.log("Hello");
}
function main() {
greet();
console.log("Done");
}
main();
调用栈变化:
- 全局上下文入栈
main()调用 →main上下文入栈greet()调用 →greet上下文入栈greet执行完 → 弹出main执行完 → 弹出- 回到全局上下文
⚠️ 如果递归太深,调用栈会溢出(Stack Overflow)!
❓ 答疑解惑 Q&A
-
Q1:变量提升是怎么回事?
var和函数声明会在代码执行前被“提升”到作用域顶部。例如:console.log(a); // undefined(不是报错!) var a = 1;实际执行顺序是:
var a; // 提升 console.log(a); // undefined a = 1; -
Q2:
let也会提升吗?
会,但不会初始化!在声明前访问会报 暂时性死区(TDZ) 错误:console.log(b); // ReferenceError let b = 2;
第四部分:闭包(Closure)—— 自由变量的“捕获者” 🔒
闭包是 JS 最强大也最令人困惑的特性之一。
🧩 什么是闭包?
闭包 = 内部函数 + 外部作用域中被引用的变量(自由变量)
当一个内部函数引用了外部函数的变量,并且这个内部函数在外部被调用时,就会形成闭包。
示例:
function foo() {
var myName = "极客时间";
function getName() {
return myName; // 引用了外部变量 myName
}
return getName;
}
const getName = foo();
console.log(getName()); // "极客时间"
即使 foo() 已经执行完毕,myName 本该被销毁,但由于 getName 引用了它,JS 引擎会将 myName 保存在 堆内存中的 closure 对象 里!
🔬 闭包的底层机制
- 编译阶段:JS 引擎扫描函数体,发现内部函数(如
getName)引用了外部变量(myName)。 - 判断闭包:引擎识别这是一个闭包场景。
- 内存分配:在堆内存中创建一个
closure(foo)对象,将myName等自由变量存入其中。 - 函数绑定:
getName的[[Scope]]内部属性指向这个closure(foo)。
🌟 关键:闭包让变量“活”得更久,突破了正常作用域的生命周期限制。
🧪 闭包的实际用途
- 模块模式(封装私有变量)
- 回调函数(如事件处理、
setTimeout) - 函数工厂
示例:函数工厂
function createCounter() {
let count = 0;
return function() {
return ++count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2 ← count 被持久保存!
❓ 答疑解惑 Q&A
- Q1:闭包会导致内存泄漏吗?
可能会!如果闭包引用了大对象,且长时间不释放,就会占用堆内存。但现代 JS 引擎的 GC 很智能,只要没有引用,就会回收。 - Q2:所有函数都是闭包吗?
严格来说,所有函数都有闭包能力,但只有当它引用了外部自由变量时,才真正形成闭包。全局函数引用全局变量不算闭包。
第五部分:作用域链与 this(补充知识)🌐
虽然本文重点是内存,但理解 作用域链 对闭包至关重要。
🔗 作用域链(Scope Chain)
- 每个执行上下文都有一个
outer指针,指向其词法父级作用域。 - 查找变量时,沿着作用域链向上搜索,直到全局。
示例:
var globalVar = "global";
function outer() {
var outerVar = "outer";
function inner() {
console.log(globalVar, outerVar); // 依次在 inner → outer → global 中查找
}
inner();
}
outer();
📌 作用域链是词法作用域(Lexical Scope)的体现——由代码书写位置决定,而非调用位置。
🎯 关于 this
this与作用域链无关!它由 函数调用方式 决定。- 不属于闭包机制,但常被混淆。
示例:
const obj = {
name: "极客时间",
getName() {
return this.name; // this 指向 obj
}
};
const fn = obj.getName;
console.log(fn()); // undefined(this 指向全局或 undefined)
✅ 记住:闭包捕获的是变量,不是 this。
❓ 答疑解惑 Q&A
- Q1:箭头函数有闭包吗?
有!但箭头函数没有自己的this和arguments,它的作用域链直接继承自外层函数。 - Q2:如何查看闭包内容?
在 Chrome DevTools 中,展开函数的[[Scopes]]属性,可以看到Closure项。
总结:一张图看懂 JS 内存与执行机制 🗺️
┌───────────────────────┐
│ 全局代码 │
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 全局执行上下文 │ ← 栈内存
│ - 变量环境 (var) │
│ - 词法环境 (let/const)│
│ - outer: null │
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 堆内存 │
│ - 对象 { ... } │
│ - closure(foo) │ ← 闭包存储自由变量
└───────────────────────┘
- 栈:快、小、存原始值 + 执行上下文
- 堆:大、慢、存对象 + 闭包
- 闭包:让外部变量“逃逸”到堆中,延长生命周期
- 执行上下文:由调用栈管理,包含变量/词法环境
- 作用域链:决定变量查找路径
结语 💫
JavaScript 的内存机制看似抽象,但一旦理解了 栈 vs 堆、值 vs 引用、执行上下文 和 闭包 的关系,你就能写出更高效、更可靠的代码。
🚀 建议:多用浏览器调试工具观察内存和作用域,实践是最好的老师!
希望这篇深度解析,能为你打开 JS 底层世界的大门。欢迎收藏、转发,也欢迎在评论区提出你的疑问!
✅ 你现在已经掌握了:
- JS 的动态弱类型本质
- 栈内存与堆内存的区别
- 执行上下文与调用栈的工作原理
- 闭包的形成机制与内存表现
- 常见误区与最佳实践
继续加油,未来的 JS 大神!💪✨