“你写的代码,真的按顺序执行了吗?”
今天不聊框架、不卷算法,来聊聊 JavaScript 底层那些“看不见”的事儿——JS 的执行机制和内存机制。
🚀 一段看似简单的 JS 代码,背后发生了什么?
先来看这段经典代码:
console.log(a); // undefined
var a = 1;
console.log(a); // 1
咦?明明 a 还没赋值,为啥不是报错,而是 undefined?
再看这个:
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 2;
同样是“提前使用”,let 就直接翻脸了?😤
这背后,就是 JavaScript 引擎(比如 Chrome 的 V8) 在“暗中操作”!而它的核心机制,离不开两个阶段:编译阶段 和 执行阶段。
🔧 阶段一:编译阶段(发生在“执行前的一刹那”)
很多人以为 JS 是纯解释型语言,写完就跑。其实不然!现代 JS 引擎(如 V8)采用的是 “即时编译”(JIT) 技术——边编译、边优化、再执行。
在真正执行代码前,V8 会先快速扫一遍代码,干几件重要的事:
✅ 1. 创建执行上下文(Execution Context)
每一段可执行代码(全局 or 函数),都会被包裹在一个 执行上下文对象 中。你可以把它想象成一个“任务包”,里面装着:
- 变量环境(Variable Environment) :存放
var声明的变量、函数声明。 - 词法环境(Lexical Environment) :存放
let/const声明的变量。 - this 指向
- 作用域链
💡 小知识:全局代码 → 全局执行上下文;函数调用 → 函数执行上下文。
✅ 2. 变量提升(Hoisting)——但 var 和 let/const 不一样!
var:在编译阶段就被“提升”到变量环境中,初始值为undefined。let/const:也会被提升,但不会初始化!它们处于“暂时性死区”(Temporal Dead Zone, TDZ),直到赋值那一刻才能访问。
所以:
console.log(a); // undefined(var 提升了)
var a = 1;
console.log(b); // ❌ 报错!
let b = 2;
🏃 阶段二:执行阶段(调用栈登场!)
编译完成后,JS 开始逐行执行。这时,调用栈(Call Stack) 成了主角!
📦 调用栈:JS 执行的“任务管理器”
- JS 是单线程的,靠 栈结构 管理函数调用。
- 全局上下文 最先入栈。
- 每调用一个函数,就创建新的执行上下文,压入栈顶。
- 函数执行完,上下文弹出栈,里面的局部变量随之销毁(后续被垃圾回收)。
举个栗子 🌰:
function fun(a) {
var b = a;
a = 2;
console.log(a, b); // 2, 3
}
fun(3);
执行过程:
-
全局上下文入栈。
-
调用
fun(3)→ 创建函数上下文,压入栈顶。- 编译阶段:
a(形参)= 3,b= undefined(var 提升) - 执行阶段:
b = a→ b = 3;a = 2→ a 变成 2
- 编译阶段:
-
函数执行完,上下文弹出,
a、b消失。
🗑️ 内存提示:函数上下文销毁 ≠ 内存立刻释放!V8 的垃圾回收器(GC)会在合适时机清理无引用的对象。
🆚 var vs let/const:不只是语法糖!
| 特性 | var | let / const |
|---|---|---|
| 提升 | ✅ 到变量环境,值为 undefined | ✅ 到词法环境,但处于 TDZ |
| 重复声明 | ✅ 允许 | ❌ 报错 |
| 作用域 | 函数级 | 块级({}) |
| 全局污染 | 会挂到 window | 不会 |
所以,现代开发请优先用 let/const!更安全、更可控。
🧩 总结:JS 执行的“三步走”
-
编译阶段(快如闪电⚡)
- 创建执行上下文
- 变量/函数提升(区分 var 和 let/const)
- 构建作用域链
-
执行阶段(按序推进▶️)
- 代码逐行运行
- 函数调用 → 新上下文入栈
- 执行完毕 → 上下文出栈
-
内存回收(默默善后🧹)
- 栈中上下文销毁
- 堆中无引用对象 → GC 回收
💬 最后说两句
JavaScript 看似简单,但它的执行机制藏着不少“小心机”。理解这些底层逻辑,不仅能帮你避开 undefined 的坑,还能在面试时自信地说:“我知道 V8 是怎么跑我代码的!” 😎
下次当你看到 Cannot access 'x' before initialization,别慌——那只是 let 在保护你,免得写出 bug 啊!