一、JavaScript 是什么语言?
JavaScript 是一门 动态弱类型语言:
- 动态语言:变量的数据类型在运行时才确定。
- 弱类型语言:允许隐式类型转换(如
"1" + 2 === "12")。
💡 对比:
- 静态强类型语言(如 C++、Java):编译期需明确类型,不能随意转换。
- 动态强类型语言(如 Python):运行时确定类型,但不允许随意转换(如
"1" + 2会报错)。
JS 的设计哲学是“灵活优先”,但也带来了类型陷阱(比如 typeof null === 'object' 这个历史 bug)。
二、JS 不需要手动管理内存
与 C/C++ 不同,JavaScript 由引擎自动管理内存:
c
编辑
// C 语言:手动申请/释放内存
int *p = malloc(sizeof(int));
*p = 42;
free(p); // 忘记这行 → 内存泄漏!
而在 JS 中:
js
编辑
let obj = { name: "极客时间" };
obj = null; // 引用断开,GC 自动回收
开发者无需调用 malloc/free,内存分配与回收由 垃圾回收器(Garbage Collector, GC) 自动处理。
三、内存空间划分:栈 vs 堆
JS 引擎(如 V8)将内存分为两类:
| 类型 | 存储内容 | 特点 |
|---|---|---|
| 栈内存(Stack) | 原始类型(Number、String、Boolean、undefined、null、Symbol、BigInt) 函数调用记录(执行上下文) | 空间小、连续、分配/释放快 生命周期短,随函数调用自动管理 |
| 堆内存(Heap) | 引用类型(Object、Array、Function 等) | 空间大、不连续、分配/回收慢 通过引用(指针)访问 |
示例对比
js
编辑
function foo() {
var a = '极客时间'; // 栈:直接存储字符串值(或指向常量池)
var b = a; // 栈:拷贝值 → b 是独立副本
var c = { name: '极客时间' }; // 栈:存的是堆地址;堆:存实际对象
var d = c; // 栈:拷贝地址 → c 和 d 指向同一堆对象
}
✅ 关键理解:
- 原始类型:值传递(拷贝值)
- 引用类型:引用传递(拷贝地址)
四、执行机制:调用栈与栈顶指针
JS 是单线程语言,靠 调用栈(Call Stack) 管理函数执行顺序。而调用栈的高效运作,离不开一个关键角色:栈顶指针(Stack Pointer) 。
什么是栈顶指针?
-
栈顶指针是一个 CPU 寄存器(在 V8 中由引擎模拟),始终指向当前栈帧的顶部。
-
每当函数被调用:
- 引擎在栈上分配一个新的执行上下文(即栈帧);
- 栈顶指针向上移动(向高地址方向偏移),指向新帧的起始位置。
-
函数执行完毕:
- 引擎弹出当前栈帧;
- 栈顶指针向下回退(恢复到上一帧的位置)。
🔧 技术细节:
在底层,栈顶指针的偏移操作是极其快速的整数加减,不需要遍历或查找,因此函数调用/返回成本极低。
为什么栈空间必须小且连续?
-
栈顶指针依赖连续内存布局进行快速偏移;
-
如果把大对象(如数组、复杂对象)也放在栈中:
- 栈帧大小变得不可预测;
- 栈顶指针频繁大幅跳动 → 破坏局部性,降低 CPU 缓存命中率;
- 极易导致 栈溢出(Stack Overflow) 。
因此,V8 将大对象统一放入堆中,栈只保留固定大小的原始值和指针,确保栈顶指针切换高效稳定。
五、执行上下文(Execution Context)包含:
- 变量环境(Variable Environment) :
var声明的变量 - 词法环境(Lexical Environment) :
let/const声明的变量(ES6+) - this 绑定
- Outer 引用:指向外层作用域(构成作用域链)
📌 作用域链 = 词法作用域 + Outer 链
由函数声明位置决定,而非调用位置。
六、闭包与内存:为什么闭包能“记住”外部变量?
闭包的本质
闭包 = 内部函数 + 被捕获的外部自由变量
当内部函数引用了外部函数的变量,JS 引擎会:
- 执行 foo 之前, 编译过程快速的词法扫描 JS 判断有闭包了,在 堆内存 中创建一个
closure(foo)对象; - 将被引用的变量(如
myName,test1)提升到堆中保存; - 即使
foo()执行完毕、调用栈弹出(栈顶指针回退),这些变量仍因被引用而不会被 GC 回收。
代码示例
js
编辑
function foo() {
var myName = "极客时间";
let test1 = 1;
const test2 = 2;
return {
setName(newName) { myName = newName; },
getName() {
console.log(test1); // 引用了 test1 → 触发闭包
return myName;
}
};
}
const bar = foo(); // foo 执行完毕,栈帧被弹出,栈顶指针回退
bar.setName("极客邦");
console.log(bar.getName()); // 输出 1 和 "极客邦"
🔍 内存视角:
foo()返回后,其栈帧被销毁(栈顶指针已回退);- 但
myName和test1因被闭包引用,存活于堆中的 closure 对象;bar持有对 closure 的引用 → 变量持续存在。
七、为什么原始类型放栈,对象放堆?
这是性能与安全的权衡:
| 考虑因素 | 栈内存 | 堆内存 |
|---|---|---|
| 大小 | 固定、小(通常几 MB) | 动态、大(可达 GB) |
| 分配速度 | 极快(栈顶指针偏移) | 较慢(需查找空闲块) |
| 连续性 | 连续(利于指针操作) | 不连续 |
| GC 成本 | 函数结束自动释放(指针回退即可) | 需标记-清除等算法 |
⚠️ 如果把大对象也放栈:
- 栈帧变大 → 栈顶指针跳动剧烈;
- 上下文切换变慢(影响函数调用性能);
- 容易栈溢出。
因此,小而固定的原始值 → 栈;大而动态的对象 → 堆,是最优解。
八、总结要点(速记版)
✅ JS 内存模型核心:
| 概念 | 说明 |
|---|---|
| 栈内存 | 存原始类型 + 函数调用帧;快、小、自动管理 |
| 堆内存 | 存对象;慢、大、靠 GC 回收 |
| 栈顶指针 | 指向当前栈帧顶部,函数调用时上移,返回时下移;是调用栈高效的关键 |
| 闭包 | 本质是堆中保存的外部变量快照,不受栈帧销毁影响 |
| 引用 vs 值 | 原始类型拷贝值,对象拷贝地址 |
| GC 触发条件 | 对象无任何引用(不可达) |
✅ 闭包三要素:
- 外部函数;
- 内部函数;
- 内部函数引用外部变量。
九、拓展思考
1. 栈顶指针与性能
现代 CPU 高度优化栈操作。V8 利用这一点,将执行上下文设计为固定大小结构体,使得栈顶指针偏移成为纳秒级操作。这也是 JS 函数调用如此高效的原因之一。
2. 闭包会导致内存泄漏吗?
可能,但不是必然。
只要闭包引用的变量最终能被释放(如解除引用),就不会泄漏。
js
编辑
// 潜在泄漏
const cache = {};
function createUser(id) {
if (!cache[id]) {
cache[id] = { /* 大对象 */ };
}
return () => cache[id]; // 闭包长期持有 cache 引用
}
// 若 cache[id] 永不删除 → 内存泄漏
✅ 建议:及时清理无用引用,尤其在长生命周期对象中