图解 JavaScript 执行原理:从编译阶段到变量提升,彻底搞懂 JS 运行机制

0 阅读8分钟

图解 JavaScript 执行原理:从编译阶段到变量提升,彻底搞懂 JS 运行机制

为什么变量可以先使用再声明?为什么函数声明可以在定义前调用?这篇文章带你深入 V8 引擎,用图解的方式彻底搞懂 JavaScript 的执行原理。

前言

JavaScript 是一门看似简单的语言,但深入理解它的执行机制却并不容易。今天课程的内容让我大开眼界——原来 JS 代码不是一行一行执行的,而是分为编译阶段执行阶段两个阶段。

这篇文章将通过图解和代码示例,带你彻底理解 JavaScript 的执行原理、变量提升机制,以及为什么 varletconst 会有如此不同的表现。


一、JavaScript 的执行流程

1.1 传统认知的误区

很多人以为 JavaScript 是按顺序一行一行执行的:

// 按直觉,下面代码应该报错?
showName();
console.log(myName);
var myName = '张三';
function showName() {
    console.log('函数showName被执行了');
}

实际运行结果:

函数showName被执行了
undefined

为什么? 因为 JavaScript 代码执行分为两个阶段:

iwEeAqNwbmcDAQTRBHYF0QDLBrD4P36kxT87zgnupZmAFwUCB9IoJU2sCAAJomltCgAL0gAAtd4.png

1.2 编译阶段 vs 执行阶段

JS 代码执行流程

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  一段JavaScript  │ ──▶ │    编译阶段      │ ──▶ │    执行阶段      │
│     代码        │     │  (准备执行上下文) │     │  (按顺序执行代码) │
└─────────────────┘     └─────────────────┘     └─────────────────┘
                              │
                              ▼
                    ┌─────────────────────┐
                    │  生成执行上下文      │
                    │  VariableEnvironment │
                    │  LexicalEnvironment  │
                    └─────────────────────┘

关键结论:

  • ✅ 在执行过程中,若使用了未声明的变量,会报错
  • ⚠️ 在一个变量定义之前使用它,不一定会报错,但值是 undefined
  • ✅ 在一个函数定义之前调用它,不会出错,且函数执行正确

💡 后两个现象表明:JS 代码不是一行一行执行的,还有编译阶段为执行阶段做准备。


二、什么是变量提升(Hoisting)?

2.1 变量提升的定义

变量提升是指在 JavaScript 代码执行过程中,JS 引擎(如 Chrome V8)把变量的声明部分函数的声明部分提升到代码开头的"行为"。变量提升后,会给变量设置默认值 undefined

2.2 模拟变量提升

iwEcAqNwbmcDAQTRBHYF0QIQBrAPzFklZ8B-pQnupZmAFwUEB9IoJU2sCAAJomltCgAL0gACt_8.png

原始代码:

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 引擎放入内存中。

iwEcAqNwbmcDAQTRBHYF0QJ6BrBSTNU_Wp9_cwnupZmAFwUDB9IoJU2sCAAJomltCgAL0gADP2Y.png


三、变量声明 vs 函数声明的提升差异

3.1 变量声明的提升

iwEdAqNwbmcDAQTRBHYF0QG3BrAV7LuspJwZzwnupZmAFwUAB9IoJU2sCAAJomltCgAL0gAA14U.png

var myname = '极客时间';

可以看成由声明和赋值两部分组成:

// 声明部分(提升到顶部)
var myname = undefined;

// 赋值部分(留在原位置)
myname = '极客时间';

3.2 函数声明 vs 函数表达式的提升差异

iwEcAqNwbmcDAQTRBHYF0QKIBrCggLM55rmDBwnupZmAFwUBB9IoJU2sCAAJomltCgAL0gABMr0.png

函数声明(完整提升)
// 函数声明 - 这是一个完整的函数声明
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 关键结论

  1. JS 是脚本语言,弱类型动态,它没有独立的编译阶段,就在运行前的那一霎那完成编译。

  2. 变量提升不是物理移动代码,而是在编译阶段将声明放入内存。

  3. 代码执行阶段是按顺序执行的,但编译阶段已经完成了内存分配。


七、实战案例分析

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 的执行原理,是成为高级前端工程师的必经之路。变量提升不是"魔法",而是编译阶段内存分配的自然结果。

掌握这些底层原理,不仅能帮你避开开发中的坑,更能让你在面试中对答如流。

希望这篇文章对你有帮助!如果有任何问题,欢迎在评论区交流。


📌 参考资源


📌 文章标签 JavaScript 变量提升 执行上下文 V8引擎 编译原理 前端面试


觉得有收获?点个赞鼓励一下吧!有问题欢迎评论区留言~ 👍