图解 JavaScript 执行原理:从编译阶段到变量提升,彻底搞懂 JS 运行机制
为什么变量可以先使用再声明?为什么函数声明可以在定义前调用?这篇文章带你深入 V8 引擎,用图解的方式彻底搞懂 JavaScript 的执行原理。
前言
JavaScript 是一门看似简单的语言,但深入理解它的执行机制却并不容易。今天课程的内容让我大开眼界——原来 JS 代码不是一行一行执行的,而是分为编译阶段和执行阶段两个阶段。
这篇文章将通过图解和代码示例,带你彻底理解 JavaScript 的执行原理、变量提升机制,以及为什么 var、let、const 会有如此不同的表现。
一、JavaScript 的执行流程
1.1 传统认知的误区
很多人以为 JavaScript 是按顺序一行一行执行的:
// 按直觉,下面代码应该报错?
showName();
console.log(myName);
var myName = '张三';
function showName() {
console.log('函数showName被执行了');
}
实际运行结果:
函数showName被执行了
undefined
为什么? 因为 JavaScript 代码执行分为两个阶段:
1.2 编译阶段 vs 执行阶段
JS 代码执行流程
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 一段JavaScript │ ──▶ │ 编译阶段 │ ──▶ │ 执行阶段 │
│ 代码 │ │ (准备执行上下文) │ │ (按顺序执行代码) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────────┐
│ 生成执行上下文 │
│ VariableEnvironment │
│ LexicalEnvironment │
└─────────────────────┘
关键结论:
- ✅ 在执行过程中,若使用了未声明的变量,会报错
- ⚠️ 在一个变量定义之前使用它,不一定会报错,但值是
undefined - ✅ 在一个函数定义之前调用它,不会出错,且函数执行正确
💡 后两个现象表明:JS 代码不是一行一行执行的,还有编译阶段为执行阶段做准备。
二、什么是变量提升(Hoisting)?
2.1 变量提升的定义
变量提升是指在 JavaScript 代码执行过程中,JS 引擎(如 Chrome V8)把变量的声明部分和函数的声明部分提升到代码开头的"行为"。变量提升后,会给变量设置默认值
undefined。
2.2 模拟变量提升
原始代码:
showName();
console.log(myname);
var myname = '极客时间';
function showName() {
console.log('showName被调用');
}
编译后(模拟):
// 变量提升部分
var myname = undefined;
function showName() {
console.log('showName被调用');
}
// 可执行代码部分
showName();
console.log(myname);
myname = '极客时间';
2.3 重要澄清:变量提升不是物理移动
"变量提升"意味着变量和函数的声明会在物理层面移动到代码的最前面——这其实并不准确。
实际上: 变量和函数声明在代码的位置是不会改变的,而是在编译阶段被 JS 引擎放入内存中。
三、变量声明 vs 函数声明的提升差异
3.1 变量声明的提升
var myname = '极客时间';
可以看成由声明和赋值两部分组成:
// 声明部分(提升到顶部)
var myname = undefined;
// 赋值部分(留在原位置)
myname = '极客时间';
3.2 函数声明 vs 函数表达式的提升差异
函数声明(完整提升)
// 函数声明 - 这是一个完整的函数声明
function foo() {
console.log('foo');
}
// 提升后:整个函数被提升到顶部
function foo() {
console.log('foo');
}
特点:
- ✅ 函数名和函数体一起提升
- ✅ 可以在声明前调用
函数表达式(仅变量提升)
// 函数表达式 - 包含变量声明和赋值
var bar = function() {
console.log('bar');
};
// 提升后:只有变量声明提升
var bar = undefined;
// 赋值留在原位置
bar = function() {
console.log('bar');
};
特点:
- ⚠️ 只有
bar变量提升,值为undefined - ❌ 在赋值前调用会报错:
bar is not a function
3.3 代码对比
// 函数声明 - 可以前置调用
showName(); // ✅ 正常执行
function showName() {
console.log('函数showName被执行了');
}
// 函数表达式 - 不能前置调用
add(); // ❌ TypeError: add is not a function
var add = function(a, b) {
return a + b;
};
四、执行上下文(Execution Context)
4.1 什么是执行上下文?
执行上下文是 JavaScript 执行一段代码的运行环境。调用一个函数,就会进入这个函数的上下文,找到上下文里的变量环境、词法环境等。
4.2 执行上下文的组成
执行上下文结构
┌─────────────────────────────────────────┐
│ 执行上下文 (Execution Context) │
├─────────────────────────────────────────┤
│ ┌─────────────────┐ ┌───────────────┐ │
│ │ 变量环境 │ │ 词法环境 │ │
│ │ Variable │ │ Lexical │ │
│ │ Environment │ │ Environment │ │
│ │ │ │ │ │
│ │ - var 声明的变量 │ │ - let/const │ │
│ │ - 函数声明 │ │ 声明的变量 │ │
│ │ - 默认值 undefined│ │ - 暂时性死区 │ │
│ └─────────────────┘ └───────────────┘ │
├─────────────────────────────────────────┤
│ 可执行代码 │
└─────────────────────────────────────────┘
4.3 变量环境 vs 词法环境
| 特性 | 变量环境 (VariableEnvironment) | 词法环境 (LexicalEnvironment) |
|---|---|---|
| 存储内容 | var 声明的变量、函数声明 | let/const 声明的变量 |
| 变量提升 | ✅ 存在 | ❌ 不存在(TDZ) |
| 声明前访问 | undefined | 报错 ReferenceError |
五、var vs let:变量提升的本质区别
5.1 核心区别
// var - 变量提升,可以前置访问
console.log(myName); // undefined
var myName = 'Darling';
// let - 也存在提升,但在词法环境中
console.log(myName); // ❌ ReferenceError
let myName = 'Darling';
5.2 本质解释
let其实也提升了,但它不和var同流合污。它的内存空间是在词法环境中。
代码执行时的分支
编译阶段:
┌─────────────────────────────────────────┐
│ var myName → 变量环境 (Variable Env) │
│ let myName → 词法环境 (Lexical Env) │
└─────────────────────────────────────────┘
执行阶段:
┌─────────────────────────────────────────┐
│ 变量环境中的变量:可以在声明前访问 │
│ → 值为 undefined │
│ │
│ 词法环境中的变量:不能在声明前访问 │
│ → 暂时性死区 (TDZ),访问会报错 │
└─────────────────────────────────────────┘
5.3 暂时性死区(TDZ)
// 暂时性死区示例
console.log(myName); // ❌ ReferenceError
let myName = 'Darling';
TDZ 范围: 从块作用域开始到声明语句之间。
{ // 块作用域开始
│
│ TDZ(暂时性死区)
│ console.log(myName) → 报错
│
▼
let myName = 'Darling'; // 声明语句
│
│ 可以正常访问
▼
} // 块作用域结束
六、变量提升的本质
6.1 一句话总结
变量提升的本质是在编译阶段完成内存的分配。
6.2 完整流程
JS 代码执行完整流程
┌─────────────────────────────────────────────────────────────┐
│ 编译阶段 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 1. 词法分析 │ │
│ │ 2. 语法分析 │ │
│ │ 3. 生成执行上下文 │ │
│ │ - 变量环境:var 变量设为 undefined │ │
│ │ - 变量环境:函数声明设为函数对象 │ │
│ │ - 词法环境:let/const 设为未初始化(TDZ) │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 执行阶段 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 按顺序执行代码: │ │
│ │ - 遇到 var 声明:跳过(已在编译阶段处理) │ │
│ │ - 遇到赋值:修改变量值 │ │
│ │ - 遇到函数调用:进入函数执行上下文 │ │
│ │ - 遇到 let/const 声明:初始化变量,退出 TDZ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
6.3 关键结论
-
JS 是脚本语言,弱类型动态,它没有独立的编译阶段,就在运行前的那一霎那完成编译。
-
变量提升不是物理移动代码,而是在编译阶段将声明放入内存。
-
代码执行阶段是按顺序执行的,但编译阶段已经完成了内存分配。
七、实战案例分析
7.1 案例一:综合提升
showName();
console.log(myName);
console.log(add);
var myName = 'Darling';
function showName() {
console.log('函数showName被执行了');
}
var add = function(a, b) {
return a + b;
};
编译后:
// 变量提升部分
var myName = undefined;
function showName() {
console.log('函数showName被执行了');
}
var add = undefined;
// 执行部分
showName(); // ✅ 函数showName被执行了
console.log(myName); // undefined
console.log(add); // undefined
myName = 'Darling';
add = function(a, b) { return a + b; };
7.2 案例二:函数内的编译
// 编译阶段一:准备好全局作用域 global{showName, myName}
showName();
console.log(myName);
var myName = '张三';
function showName() {
// 编译阶段二:为 showName 执行而准备
var a = 1;
console.log('函数showName被执行了');
}
关键点: 调用函数时,会进入该函数的执行上下文,进行第二轮编译。
八、知识图谱
📚 JavaScript 执行原理知识图谱
JS 执行流程
├── 编译阶段
│ ├── 词法分析
│ ├── 语法分析
│ └── 生成执行上下文
│ ├── 变量环境 (VariableEnvironment)
│ │ ├── var 声明 → undefined
│ │ └── 函数声明 → 函数对象
│ └── 词法环境 (LexicalEnvironment)
│ └── let/const → 未初始化 (TDZ)
│
└── 执行阶段
└── 按顺序执行代码
变量提升 (Hoisting)
├── 本质:编译阶段完成内存分配
├── 不是物理移动代码
├── 变量声明提升 → 值设为 undefined
├── 函数声明提升 → 整个函数提升
└── 函数表达式 → 仅变量提升
var vs let/const
├── var → 变量环境 → 可前置访问 → undefined
├── let/const → 词法环境 → TDZ → 报错
└── 暂时性死区 (TDZ):声明前不可访问
常见错误
├── ReferenceError: XXX is not defined
│ └── 变量从未声明
├── ReferenceError: Cannot access 'X' before initialization
│ └── let/const TDZ 访问
└── TypeError: X is not a function
└── 函数表达式前置调用
九、面试高频题速查
Q1:变量提升是什么?
JS 引擎在编译阶段将变量和函数声明放入内存,使它们可以在实际声明之前被访问。变量提升后,
var变量默认值为undefined。
Q2:函数声明和函数表达式的提升有什么区别?
函数声明会整体提升(函数名+函数体),可以在声明前调用。函数表达式只有变量提升(值为
undefined),在赋值前调用会报错。
Q3:let 有变量提升吗?
有,但
let的声明在词法环境中,不在变量环境中。在声明前访问会进入暂时性死区(TDZ),报ReferenceError。
Q4:变量提升的本质是什么?
本质是在编译阶段完成内存分配,不是物理移动代码位置。
结语
理解 JavaScript 的执行原理,是成为高级前端工程师的必经之路。变量提升不是"魔法",而是编译阶段内存分配的自然结果。
掌握这些底层原理,不仅能帮你避开开发中的坑,更能让你在面试中对答如流。
希望这篇文章对你有帮助!如果有任何问题,欢迎在评论区交流。
📌 参考资源
- MDN: Hoisting
- V8 引擎官方文档
- 《JavaScript 高级程序设计》第 4 章
📌 文章标签
JavaScript变量提升执行上下文V8引擎编译原理前端面试
觉得有收获?点个赞鼓励一下吧!有问题欢迎评论区留言~ 👍