为什么
console.log(a)在var a = 1之前输出的是undefined而不是报错?
为什么let声明的变量不能提升?
函数声明和变量声明谁优先?
JS 是如何一步步执行代码的?
这些问题的答案,都藏在 JavaScript 的执行机制 中。本文将带你从底层理解 JS 是如何被浏览器(以 Chrome 的 V8 引擎为例)编译和执行的。
一、JS 不是“解释型”那么简单
很多人说 JavaScript 是“解释型语言”,但现代 JS 引擎(如 V8)早已采用 即时编译(JIT, Just-In-Time Compilation) 技术——即 先编译,再执行。
虽然不像 Java 或 C++ 那样提前编译成字节码或机器码,但 JS 在执行前会经历一个极快的 编译阶段,这个阶段发生在执行的“一霎那”。
二、JS 执行的两个核心阶段
1. 编译阶段(Compilation Phase)
-
目的:为后续执行做准备。
-
主要工作:
- 检查语法错误(Syntax Error)
- 创建 执行上下文(Execution Context)
- 进行 变量提升(Hoisting)
执行上下文包含什么?
每个执行上下文包含两个关键环境:
| 环境类型 | 存放内容 |
|---|---|
| 变量环境(Variable Environment) | var 声明的变量、函数声明 |
| 词法环境(Lexical Environment) | let、const 声明的变量(处于“暂时性死区”) |
📌 注意:函数声明会被完整提升(包括函数体),而
var只提升变量名并初始化为undefined;let/const虽然也被“提升”,但不会初始化,在声明前访问会报错(TDZ)。
2. 执行阶段(Execution Phase)
- 按照代码书写顺序,逐行执行。
- 此时变量已被分配内存,函数可被调用。
- 如果遇到函数调用,会创建新的执行上下文,并压入 调用栈(Call Stack) 。
三、调用栈:JS 执行的“调度中心”
V8 引擎使用 调用栈 来管理函数的执行顺序。
- 全局执行上下文 最先被压入栈底。
- 每调用一个函数,就创建一个新的执行上下文,压入栈顶。
- 函数执行完毕后,其上下文出栈,相关变量可能被垃圾回收。
✅ 栈的特点:后进先出(LIFO)。这保证了函数调用的嵌套逻辑正确执行。
四、变量提升详解:var vs let/const
示例 1:var 的提升
console.log(myName); // undefined
var myName = '陈倩文';
编译阶段:
var myName;→ 提升到顶部,初始化为undefined- 执行阶段:
myName = '陈倩文'赋值
示例 2:函数声明优先级更高
showName(); // "函数showName被执行"
console.log(myName); // undefined
var myName = '陈倩文';
function showName() {
console.log('函数showName被执行');
}
编译阶段处理顺序:
- 函数声明
showName被完整提升(优先于变量) var myName被提升为undefined
💡 规则:函数声明 > var 变量声明
示例 3:let 的暂时性死区(TDZ)
console.log(hero); // ReferenceError!
let hero = '钢铁侠';
let hero在编译阶段被放入 词法环境,但未初始化。- 在
let声明前访问 → 处于 暂时性死区(Temporal Dead Zone) → 报错!
五、函数执行上下文的创建过程
以如下函数为例:
var a = 1;
function fn(a) {
var a = 2;
var b = a;
console.log(a); // 2
}
fn(3);
全局上下文(编译阶段):
var a→a = undefinedfunction fn→ 完整提升
调用 fn(3) 时(执行阶段):
-
创建 fn 的执行上下文,压入调用栈
-
编译阶段(函数内部) :
- 形参
a被设为3 var a声明 → 与形参同名,覆盖形参(但值仍为3,直到赋值)var b→b = undefined
- 形参
-
执行阶段:
var a = 2→a被赋值为2var b = a→b = 2- 输出
2
⚠️ 注意:
var允许重复声明,不会报错,但会覆盖(或忽略)。
六、var vs let/const 对比总结
| 特性 | var | let / const |
|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 {} |
| 提升 | 提升并初始化为 undefined | 提升但不初始化(TDZ) |
| 重复声明 | 允许(静默忽略) | 不允许(SyntaxError) |
| 全局对象属性 | 是(如 window.a) | 否 |
七、值类型 vs 引用类型:内存模型补充
虽然不属于执行机制主线,但常被混淆,顺带说明:
// 基本类型(值拷贝)
let str = 'hello';
let str2 = str;
str2 = '你好';
console.log(str, str2); // 'hello' '你好'
// 引用类型(地址拷贝)
let obj = { name: '郑老板' };
let obj2 = obj;
obj2.name = '郑总';
console.log(obj.name); // '郑总'
- 基本类型(string, number, boolean 等):存储在 栈内存,赋值是值拷贝。
- 引用类型(object, array, function):实际数据存在 堆内存,变量保存的是 地址引用。
八、总结:JS 执行流程全景图
-
代码加载 → V8 引擎接管
-
编译阶段(极快):
- 创建全局执行上下文
- 语法检查
- 变量/函数提升(按规则)
-
执行阶段:
- 从上到下执行代码
- 遇到函数调用 → 创建新上下文 → 压入调用栈
- 函数执行完 → 上下文出栈 → 内存回收
-
全程依赖调用栈管理执行顺序
✅ 关键记忆点:
- 先编译,后执行
- 函数声明 > var > let/const(TDZ)
- 调用栈 = 执行上下文的容器
- var 有坑,let/const 更安全
掌握这套机制,不仅能解释各种“诡异”的 JS 行为,还能写出更可靠、可维护的代码。建议多结合调试工具(如 Chrome DevTools 的断点)观察调用栈变化,加深理解。
如有需要,我还可以提供配套练习题或可视化执行流程图!