# 从变量提升到调用栈：V8 引擎如何 “读懂” JS 代码

**URL:** <https://www.shiqidu.com/t/topic/871>\
**Category:** 开发调优\
**Tags:** JavaScript\
**Created:** [2025 年11 月 8 日 16:25 UTC](https://www.shiqidu.com/t/topic/871 "2025-11-08T16:25:57Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![shiqidu\_editor](https://www.shiqidu.com/user_avatar/www.shiqidu.com/shiqidu_editor/32/528_2.png) [@shiqidu\_editor](https://www.shiqidu.com/u/shiqidu_editor)\
发布日期： [2025 年11 月 8 日 16:25 UTC](https://www.shiqidu.com/t/topic/871/1 "2025-11-08T16:25:57Z")

</div>

在 Chrome 浏览器中，JavaScript 代码的编译和执行全靠 **V8 引擎** 。不同于 C++、Java 等编译型语言，JS 作为脚本语言，编译过程发生在执行前的一霎那 —— 这也造就了它独特的执行逻辑： **代码编写顺序和实际执行顺序往往并不一致** 。

今天我们就从代码实例出发，一步步拆解 V8 引擎如何处理 JS 代码，把变量提升、执行上下文、调用栈这些核心概念讲明白～

## 一、先编译，后执行：V8 引擎的 “预加工” 操作

JS 代码运行时，V8 引擎不会直接逐行执行，而是先进入 **编译阶段** 做准备工作，再进入 **执行阶段** 运行代码。编译阶段的核心任务有两个：

1. 检测语法错误（比如漏写括号、变量未声明等）；

2. 变量提升（提前识别变量和函数，为执行阶段铺路）。

### 看个直观例子

```js
// 先调用函数、访问变量，再定义它们
showName();
console.log(myName);
console.log(hero);
var myName = 'Asdj';
let hero = '钢铁侠';
function showName() {
    console.log('函数showName被执行'); 
}

```

按 “从上到下” 的编写逻辑，showName() 和 console.log(myName) 早该报错，但实际运行结果是：

- showName() 正常执行（输出 “函数 showName 被执行”）；

- console.log(myName) 输出 undefined；

- console.log(hero) 报错（Cannot access ‘hero’ before initialization）。

这就是 **变量提升** 在起作用 ——V8 引擎在编译时，会把 var 声明的变量和函数声明 “提前” 到当前作用域顶部。

### 变量提升的规则

V8 引擎编译时，对不同声明的 “提升逻辑” 不同：

| 声明类型 | 提升行为 |
| --- | --- |
| 函数声明 | 优先级最高， **完整提升整个函数体** （可以在定义前直接调用） |
| var 变量声明 | 只提升 “声明”，不提升 “赋值”，初始值设为 undefined |
| let/const 声明 | **不提升** ，会进入 “暂时性死区”（TDZ），执行阶段前访问会直接报错 |

### 编译后的执行逻辑

上面的代码经过编译后，实际执行顺序是这样的：

```javascript
// 编译阶段：提升的内容（开发者看不到，是引擎内部操作）
function showName(){ // 函数声明完整提升
    console.log('函数showName被执行');
}
var myName; // var变量只提升声明，初始值undefined
// 执行阶段：按编译后的顺序执行
showName(); // 正常调用（函数已提升）
console.log(myName); // 输出undefined（只声明未赋值）
console.log(hero); // 报错（let未提升，处于暂时性死区）
myName = 'Asdj'; // 执行赋值操作（编译阶段不处理赋值）
let hero = '钢铁侠'; // let声明和赋值在执行阶段进行

```

### 为什么需要变量提升？

本质是为了适配 JS “ **边编译、边执行** ” 的脚本语言特性：

编译阶段提前识别变量和函数，能避免执行时因 “变量未定义” 导致的逻辑断裂，同时允许函数声明在定义前调用，提升代码编写的灵活性。

## 二、执行上下文：代码运行的 “环境容器”

编译阶段的核心产物是 **执行上下文（Execution Context）** —— 它就像一个 “环境容器”，包含了代码执行所需的所有信息（变量、函数、作用域、this 等）。

V8 引擎会为不同类型的代码创建对应的执行上下文：

- **全局执行上下文** ：全局代码（不在任何函数内的代码）对应的上下文，页面加载时创建，直到页面关闭才销毁；

- **函数执行上下文** ：函数代码对应的上下文， **每次调用函数时都会创建一个全新的实例** ，函数执行完毕后就会被销毁（垃圾回收）。

### 执行上下文的组成

每个执行上下文都包含 3 个核心部分：

1. **变量环境（Variable Environment）** ：存储 var 声明的变量和函数声明，允许重复声明；

2. **词法环境（Lexical Environment）** ：存储 let/const 声明的变量，不允许重复声明，存在暂时性死区；

3. **可执行代码（Executable Code）** ：编译后等待执行的代码（去除了声明提升部分）。

### 用代码拆解执行上下文流程

我们用一段带函数的代码，一步步看执行上下文的创建、执行、销毁过程：

```js
var a = 1;
function fn(a) {
  console.log(a);
  var a = 2;
  var b = a;
  console.log(a);
}
fn(3);
console.log(a);

```

#### 步骤 1：全局执行上下文（编译阶段）

V8 引擎先编译全局代码，创建全局执行上下文：

- 变量环境：a: undefined（var a 提升声明）、fn: 函数体（函数声明完整提升）；

- 词法环境：空（无 let/const 声明）；

- 可执行代码：全局代码（赋值、函数调用等逻辑）。

#### 步骤 2：全局执行上下文（执行阶段）

全局执行上下文被压入调用栈后，开始执行代码：

1. 执行 var a = 1：变量环境中 a 的值从 undefined 更新为 1；

2. 执行 fn(3)：调用函数，触发 **函数执行上下文** 的创建。

#### 步骤 3：函数执行上下文（编译阶段）

编译 fn 函数内部代码，创建函数执行上下文：

- 变量环境：形参 a: 3（实参赋值给形参）、b: undefined（var b 提升声明）；

- 词法环境：空（无 let/const 声明）；

- 可执行代码：函数内部代码（console.log(a)、var a = 2 等）。

#### 步骤 4：函数执行上下文（执行阶段）

函数执行上下文被压入调用栈顶（优先执行），开始执行内部代码：

1. 执行 console.log(a)：读取变量环境中的形参 a，输出 3；

2. 执行 var a = 2：变量环境中 a 的值从 3 更新为 2；

3. 执行 var b = a：变量环境中 b 的值设为 2；

4. 执行 console.log(a)：读取变量环境中的 a，输出 2；

5. 函数执行完毕：函数执行上下文从调用栈中弹出，被垃圾回收销毁。

#### 步骤 5：回到全局执行上下文

继续执行全局代码：

- 执行 console.log(a)：读取全局变量环境中的 a，输出 1。

最终运行结果：3 → 2 → 1—— 这正是执行上下文 “创建 → 执行 → 销毁” 的完整流程体现。

## 三、调用栈：管理执行上下文的 “调度中枢”

V8 引擎用 **调用栈（Call Stack）** 来管理所有执行上下文 —— 调用栈是一种 “先进后出（LIFO）” 的数据结构，能确保代码按正确顺序执行。

### 调用栈的工作流程

1. 页面加载时， **全局执行上下文** 先被压入栈底；

2. 调用函数时，创建对应的 **函数执行上下文** 并压入栈顶；

3. 栈顶的执行上下文（当前正在执行的代码）优先执行；

4. 函数执行完毕，其执行上下文从栈顶弹出并销毁；

5. 所有代码执行完毕，调用栈中只剩全局执行上下文，页面关闭时弹出销毁。

### 用例子理解调用栈变化

```javascript
function a() {
  console.log('a执行');
  b(); // 调用函数b
}
function b() {
  console.log('b执行');
}
a(); // 调用函数a

```

调用栈的变化过程（可视化）：

| 执行步骤 | 调用栈状态 | 说明 |
| --- | --- | --- |
| 1 | [全局执行上下文] | 页面加载，全局上下文入栈 |
| 2 | [全局，a] | 调用 a ()，a 的上下文入栈 |
| 3 | [全局，a, b] | a 中调用 b ()，b 的上下文入栈 |
| 4 | [全局，a] | b 执行完毕，出栈 |
| 5 | [全局] | a 执行完毕，出栈 |
| 6 | [] | 页面关闭，全局上下文出栈 |

运行结果：a执行 → b执行—— 完全符合调用栈 “栈顶优先执行” 的规则。

## 四、var、let/const 的核心区别：从编译角度看

var 和 let/const 的差异，本质是 **编译阶段存储位置不同** （变量环境 vs 词法环境），我们用代码直观感受：

```javascript
// 1. 访问var和let变量
console.log(a); // undefined（变量环境，提升后初始值undefined）
console.log(b); // 报错（词法环境，暂时性死区）
// 2. 重复声明
var a = 1;
var a = 2; // var允许重复声明，覆盖值
console.log(a); // 输出2
let b = 3;
// let b = 4; // let不允许重复声明，报错
console.log(b); // 输出3
// 3. 严格模式下的var
'use strict';
var c = 1;
var c = 2; // 严格模式下，var仍允许重复声明
console.log(c); // 输出2
// 4. 函数表达式
func(); // 报错：func is not a function（函数表达式不提升）
let func = () => {
    console.log('函数表达式不会提升');
}

```

### 核心区别总结

| 特性 | var | let/const |
| --- | --- | --- |
| 提升行为 | 提升声明，初始值 undefined | 不提升，进入暂时性死区 |
| 重复声明 | 允许（无报错，覆盖值） | 不允许（直接报错） |
| 存储位置 | 变量环境 | 词法环境 |
| 函数表达式 | 仅提升变量声明（值为 undefined） | 不提升（调用时报错） |

## 五、数据类型存储：栈内存与堆内存的差异

JS 中简单数据类型和复杂数据类型的存储方式不同，导致赋值时出现 “值拷贝” 和 “引用拷贝” 的差异 —— 本质是 **栈内存** 和 **堆内存** 的使用逻辑不同。

### 先看代码例子

```js
// 1. 简单数据类型（字符串、数字、布尔等）
let str = 'hello';
let str2 = str; // 值拷贝：复制栈内存中的值
str2 = '你好'; // 修改str2的栈内存值，不影响str
console.log(str, str2); // 输出：hello 你好
// 2. 复杂数据类型（对象、数组、函数等）
let obj = { 
    name: '老板',
    age: 18
};
let obj2 = obj; // 引用拷贝：复制栈内存中的地址（指向堆内存数据）
obj2.age++; // 通过地址修改堆内存数据，obj也受影响
console.log(obj2, obj); // 两者age都是19

```

### 栈内存与堆内存的区别

| 内存类型 | 特点 | 存储内容 |
| --- | --- | --- |
| 栈内存 | 空间小、读取快 | 简单数据类型的 “值”、复杂数据类型的 “地址” |
| 堆内存 | 空间大、存储复杂 | 复杂数据类型的 “实际数据”（如对象的键值对） |

简单理解：

- 简单数据类型：变量直接持有 “值”（存在栈里），赋值时复制 “值”；

- 复杂数据类型：变量持有 “地址”（存在栈里），“地址” 指向堆内存中的实际数据，赋值时复制 “地址”（两个变量指向同一堆数据）。

## 总结：V8 引擎执行 JS 的完整流程

V8 引擎执行 JS 代码的全过程可以概括为 4 步：

1. **编译阶段** ：接管代码，检测语法错误，创建执行上下文（变量环境存 var 和函数声明，词法环境存 let/const），完成变量提升；

2. **调用栈调度** ：全局执行上下文压入栈底，函数调用时创建函数执行上下文并压入栈顶；

3. **执行阶段** ：栈顶执行上下文对应的代码逐行执行（变量赋值、函数调用等）；

4. **销毁阶段** ：函数执行完毕，其执行上下文出栈并销毁，全局执行上下文在页面关闭时销毁。

> 本文转自 [https://juejin.cn/post/7569825640851570723](https://juejin.cn/post/7569825640851570723)，如有侵权，请联系删除。
