解密 JS 变量提升:告别玄学,读懂 V8 编译与代码执行逻辑

前言

在日常的 JavaScript 开发中,你是否遇到过这样的困惑:为什么在变量声明之前访问它不会报错,而是返回 undefined?为什么在函数定义之前调用它却能正常执行?这些看似 "反直觉" 的行为背后,隐藏着 JavaScript 引擎运行机制的核心秘密 ------变量提升(Hoisting)

本文将从现象出发,深入 JavaScript 引擎(Chrome V8)的执行原理,带你彻底搞懂变量提升的本质,以及 JS 代码的编译与执行流程。


一、从一个 "奇怪" 的例子说起

请看下面这段代码:

javascript 复制代码
// 函数声明
showName();
console.log(myName);
console.log(add);
var myName = '极客时间';
// 传统的函数声明
function showName(){
    console.log('函数showName被执行了');
}

// 匿名函数 / 函数表达式
var add = function(a, b){
    return a + b;
}

执行结果:

javascript 复制代码
函数showName被执行了
undefined
undefined

这段代码暴露了三个问题:

  1. 函数 showName 在定义之前被调用,却没有报错,而是正常执行了。
  2. 变量 myName 在声明之前被访问,没有报错,但值是 undefined ,而不是 '极客时间'
  3. 变量 add 在赋值之前被访问,同样是 undefined ,但它被赋值为一个函数,却无法在赋值前调用。

按照传统的编程思维,代码应该是一行一行顺序执行 的。如果真是一行一行执行,那么第 1 行 showName() 和第 2 行 console.log(myName) 都应该报错 ReferenceError,因为此时它们还没有被定义。

核心现象:JS 代码并非单纯自上而下逐行执行,声明类代码会在编译阶段提前处理,这是变量提升的核心前提。

但事实并非如此。

但事实并非如此。


二、传统认知 vs 实际情况

2.1 传统认知:代码按顺序执行

大多数开发者初学 JS 时都认为:JavaScript 代码就是从上到下一行一行执行的。按照这个逻辑:

  • showName() 调用时,函数尚未定义 → 应该报错
  • console.log(myName) 访问时,变量尚未声明 → 应该报错

但实际运行结果推翻了这一认知。

2.2 实际表现总结

场景 表现 是否报错
使用未声明的变量 报错 ReferenceError ✅ 报错
在变量定义之前使用它 不报错,值为 undefined ❌ 不报错
在函数定义之前调用它 不报错,函数正常执行 ❌ 不报错

后两种情况说明:JavaScript 代码并不是简单的一行一行执行的。


三、JS 代码执行的两阶段模型

3.1 先编译,后执行

JavaScript 虽然是脚本语言(弱类型、动态),没有像 Java/C++ 那样独立的、漫长的 "编译期",但它在代码执行前的瞬间 ,仍然会经历一个短暂的编译阶段

整个过程分为两个阶段:

  1. 编译阶段(Compilation Phase)
  2. 执行阶段(Execution Phase)

📌 图 1:JavaScript 代码执行的两阶段流程

3.2 编译阶段做了什么?

输入一段 JavaScript 代码,经过编译后,会生成两部分内容:

  • 执行上下文(Execution Context) :JS 执行一段代码的运行环境
  • 可执行代码(Executable Code) :编译后的字节码

核心重点 :变量提升不属于执行阶段的行为,所有变量、函数的提升动作,全部发生在编译阶段

变量提升就是在编译阶段发生的。 在这个阶段,JavaScript 引擎会:

  1. 扫描整个代码,找到所有 var 变量声明
  2. 扫描所有 function 函数声明
  3. 将这些声明提升到作用域的顶部,并分配内存空间

具体来说:

提升规则(核心)

  • var 变量 :仅提升声明,编译期初始化为undefined,赋值留在原执行位置
  • 函数声明 function:完整提升(声明+函数体),编译期直接赋值为函数对象
  • var 声明的变量 :提升变量名,初始化为 undefined
  • 函数声明 function:整体提升,包括函数体,初始化为函数对象本身
  • var 声明的变量 :提升变量名,初始化为 undefined
  • 函数声明 function:整体提升,包括函数体,初始化为函数对象本身

3.3 用代码模拟编译后的效果

原始代码经过编译阶段后,逻辑上等价于以下代码:

编译阶段后 ------ 变量环境部分:

javascript 复制代码
// 变量提升部分的代码(编译阶段完成)
var myname = undefined;
function showName(){
    console.log('函数showName被执行了');
}

执行阶段代码:

javascript 复制代码
// 执行阶段的代码
showName();
console.log(myname);
myname = '极客时间';

将两者合并,完整的执行等价于:

javascript 复制代码
var myname = undefined;
function showName() {
    console.log('showName被执行了');
}
showName();
console.log(myname);
myname = '极客时间';

📌 图 2:变量提升前后的代码逻辑对比

这样一看,代码的行为就完全合理了 ------变量和函数的声明被 "移动" 到了代码的顶部 。但需要注意,这种 "移动" 只是逻辑上的理解方式,实际上代码的位置并没有改变,只是 JavaScript 引擎在编译阶段将声明放入了内存中。

关键纠正 :变量提升无物理位置移动,本质是编译阶段提前分配内存、注册标识符


四、变量提升的完整机制

4.1 什么是变量提升?

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

4.2 深入理解:不仅仅是 "移动代码"

很多教程说 "变量和函数声明会被物理移动到代码最前面"------ 这种说法并不准确

实际情况是:

  1. 变量和函数声明在代码中的物理位置并没有改变
  2. 编译阶段 ,JS 引擎将它们放入内存
  3. 在执行阶段,JS 引擎可以直接从内存中访问这些变量和函数

对于 var 声明的变量来说,JS 引擎会将其拆分为声明赋值两个独立的步骤:声明阶段在编译期完成并被提升,赋值阶段则保留在原代码位置,在执行期完成。

📌 图 3:var 声明的拆分:声明提升,赋值留在原地

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

这是一个非常重要的区别:

javascript 复制代码
// 函数声明 ------ 整体提升
function showName(){
    console.log('函数showName被执行了');
}

// 函数表达式 ------ 只有变量名提升,函数体不提升
var add = function(a, b){
    return a + b;
}

高频易错核心:函数声明 vs 函数表达式

  • 函数声明:整体提升,声明前可正常调用
  • 函数表达式 :仅变量名提升(值为undefined),函数体不提升,声明前调用报TypeError
  • 函数声明 function showName(){}:整个函数(包括函数体)被提升,所以在声明前调用也可以正常执行。
  • 函数表达式 var add = function(){}:只有变量名 add 被提升(值为 undefined),函数体并没有被提升。所以在赋值前调用add() 会报错 TypeError: add is not a function

📌 图 4:函数声明与函数表达式的提升差异

4.4 为什么函数是一等对象?

在 JavaScript 中,函数是一等公民(First-Class Object)

  • 函数可以被赋值给变量
  • 函数可以作为参数传递给其他函数
  • 函数可以作为其他函数的返回值

正因为函数的这种特性,函数声明在编译阶段被完整提升,包括其函数体代码。这使得我们在代码的任何位置都可以调用在后续位置声明的函数。


五、var 与 let/const 的提升差异 ------ 暂时性死区(TDZ)

5.1 let 和 const 也有提升吗?

有,但与 var 的行为不同。 来看这段代码:

重磅误区纠正let/const存在变量提升,并非不提升!区别是提升后不初始化,进入暂时性死区。

javascript 复制代码
console.log(myName);  // ReferenceError: Cannot access 'myName' before initialization
let myName = '极客时间';

5.2 三种声明的提升行为对比

声明方式 是否提升 初始化值 声明前访问
var ✅ 提升 undefined 返回 undefined
function ✅ 提升(含函数体) 函数对象 正常执行
let ✅ 提升(但进入 TDZ) 无(未初始化) 报错 ReferenceError
const ✅ 提升(但进入 TDZ) 无(未初始化) 报错 ReferenceError

5.3 什么是暂时性死区(Temporal Dead Zone, TDZ)?

letconst 声明的变量确实会被提升 ,但它们被提升后并不会被初始化 。从作用域顶部到变量实际声明位置之间的区域,被称为暂时性死区(TDZ)

TDZ 核心规则 :变量提升后未完成初始化,在声明语句执行前,该变量处于死区,任何访问都会直接报错 ,而非 undefined

在 TDZ 内访问变量会抛出 ReferenceError: Cannot access 'variable' before initialization

在 TDZ 内访问变量会抛出 ReferenceError: Cannot access 'variable' before initialization

javascript 复制代码
{
    // TDZ 开始
    console.log(x);  // ❌ ReferenceError
    // TDZ 继续
    let x = 10;      // TDZ 结束
    console.log(x);  // ✅ 10
}

5.4 变量环境 vs 词法环境

为什么 varlet/const 的行为不同?根本原因在于它们存储在不同的内存空间中:

  • 变量环境(Variable Environment) :存储 varfunction 声明。在编译阶段完成内存分配并初始化为 undefined
  • 词法环境(Lexical Environment) :存储 letconst 声明。在编译阶段完成内存分配但不初始化,处于 TDZ 状态。

在执行代码时:

  • 变量环境中的声明可以在声明前访问 (值为 undefined
  • 词法环境中的声明不能在声明前访问(会报错)

变量提升的本质,就是在编译阶段完成内存分配的过程。

终极本质总结 : 1. 变量环境(var/function):编译期分配内存 + 自动初始化为 undefined/函数对象 2. 词法环境(let/const):编译期分配内存 + 不初始化 + 进入 TDZ


六、深入理解执行上下文(Execution Context)

6.1 什么是执行上下文?

执行上下文(Execution Context) 是 JavaScript 引擎执行一段代码时所创建的运行环境。它包含了代码执行所需的所有信息:

  • 变量环境(Variable Environment)
  • 词法环境(Lexical Environment)
  • this 绑定
  • 对外部环境的引用(作用域链)

6.2 执行上下文的生命周期

  1. 创建阶段(编译阶段)

    1. 创建变量环境(var 声明 → undefined
    2. 创建词法环境(let/const 声明 → 未初始化,进入 TDZ)
    3. 确定 this 指向
    4. 建立作用域链
  2. 执行阶段

    1. 逐行执行代码
    2. 变量赋值
    3. 函数调用
  3. 销毁阶段

    1. 执行上下文从调用栈中弹出
    2. 垃圾回收

6.3 调用栈(Call Stack)

JavaScript 是单线程语言,使用调用栈来管理执行上下文的创建与销毁:

  • 全局执行上下文始终位于栈底
  • 每次调用一个函数,就创建一个新的执行上下文并压入栈顶
  • 函数执行完毕,其执行上下文从栈顶弹出

七、完整的代码执行流程示例

让我们用一个更完整的例子来串联所有知识点:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    Hoisting Demo
</head>
<body>
    <script>
        showName();
        console.log(myName);
        function showName() {
            console.log('函数showName被执行了');
        }
    </script>
</body>
</html>

这段代码的执行流程如下:

第一步:编译阶段

JavaScript 引擎扫描 <script> 标签内的代码:

  1. 发现 function showName(){} 函数声明 → 将其放入变量环境,完整保留函数体
  2. 未发现 var 声明(该示例中 myName 未被声明)

编译完成后的变量环境:

javascript 复制代码
变量环境: {
    showName: function() { console.log('函数showName被执行了'); }
}

第二步:执行阶段

  1. 执行 showName() → 在变量环境中找到 showName → 正常调用 → 输出 '函数showName被执行了'
  2. 执行 console.log(myName) → 变量环境中没有 myName,作用域链中也找不到 → 抛出 ReferenceError: myName is not defined

注意: 这与使用 var 声明的情况不同。如果是var myName,编译阶段会将其初始化为 undefined,访问时不会报错。

关键区别 :未声明变量访问报 ReferenceError: xxx is not definedvar 声明未赋值变量访问返回 undefined

📌 图 5:完整的代码编译与执行全流程


八、常见误区与最佳实践

8.1 常见误区

误区一:变量提升是 "物理移动代码"

三大高频误区汇总

  • 误区1:代码物理移动 → 正确:内存预分配,代码位置不变
  • 误区2:let/const 无提升 → 正确:有提升,受 TDZ 限制
  • 误区3:函数表达式可前置调用 → 正确:仅函数声明可前置调用
  • ❌ 错误理解:JS 引擎真的把变量声明移动到了代码顶部
  • ✅ 正确理解:编译阶段在内存中提前创建了变量,代码位置不变

误区二:let 和 const 没有提升

  • ❌ 错误理解:let/const 声明的变量完全不提升
  • ✅ 正确理解:let/const 有提升,但进入暂时性死区(TDZ),声明前访问会报错

误区三:函数表达式和函数声明的提升一样

  • ❌ 错误理解:var foo = function(){}function foo(){} 提升效果相同
  • ✅ 正确理解:函数声明整体提升;函数表达式只提升变量名,函数体不提升

8.2 最佳实践

  1. 优先使用 const ,其次 let ,避免使用 var constlet 有 TDZ 保护,能在声明前使用时主动报错,更早暴露问题
  2. 变量声明放在作用域顶部即使有提升机制,也应该在代码顶部声明变量,保持代码可读性
  3. 函数声明放在调用之前虽然函数声明可以整体提升,但良好的代码组织胜过依赖语言特性
  4. 理解原理,但不依赖提升写出清晰、可预测的代码,而不是利用语言特性 "炫技"

核心最佳实践宗旨:利用 TDZ 规避隐性 bug,规范化代码书写,不依赖变量提升的语法特性写代码。


九、总结

核心要点回顾

  1. JavaScript 代码执行分为两个阶段:编译阶段和执行阶段。编译阶段在代码执行前的瞬间完成。
  2. 变量提升发生在编译阶段 :JS 引擎会扫描代码,将 varfunction 声明提升到作用域顶部,并分配内存。
  3. var 声明的变量 :提升变量名,初始化为 undefined
  4. 函数声明:整体提升(包括函数体),可以直接调用。
  5. 函数表达式 :只有变量名提升(值为 undefined),函数体不提升。
  6. let 和 const:也有提升,但存储在词法环境中,声明前处于暂时性死区(TDZ),访问会报错。
  7. 变量提升的本质:是在编译阶段完成内存分配的过程,而不是物理移动代码。
  8. 变量环境 vs 词法环境var/function 存在变量环境中(可在声明前访问);let/const 存在词法环境中(不可在声明前访问)。

一张图总结

javascript 复制代码
JavaScript 代码执行流程:

源代码
  │
  ▼
┌──────────────────────────────┐
│        编译阶段 (Compilation)  │
│                               │
│  1. 词法分析 (Tokenization)    │
│  2. 语法分析 (Parsing)         │
│  3. 创建执行上下文             │
│     ├─ 变量环境 (var/function) │  ← 变量提升发生在这里
│     └─ 词法环境 (let/const)    │  ← let/const 进入 TDZ
│                               │
└──────────┬───────────────────┘
           │
           ▼
┌──────────────────────────────┐
│        执行阶段 (Execution)    │
│                               │
│  1. 逐行执行代码               │
│  2. 变量赋值                   │
│  3. 函数调用                   │
│  4. 调用栈管理                 │
│                               │
└──────────────────────────────┘

理解 JavaScript 的执行原理,尤其是变量提升机制,不仅能帮助我们写出更可靠的代码,还能让我们在遇到诡异 Bug 时,快速定位问题根源。希望本文能帮助你彻底理解这一核心概念!

相关推荐
东风破_1 小时前
一文搞懂 JavaScript 变量声明:var、let、const 到底有什么区别?
前端·javascript
无糖可可果1 小时前
拆穿 JavaScript 变量提升的"魔术"——从一段反直觉代码说起
javascript
月光刺眼1 小时前
🎶二分 · 双指针 · 滑动窗口 · 螺旋矩阵:数组算法四题拆解
javascript·算法
光影少年2 小时前
Redux Toolkit 用法、解决原生Redux 冗余问题
开发语言·前端·javascript·react.js·中间件·前端框架·ecmascript
云水一下2 小时前
JavaScript 从零基础到精通系列:DOM 操作与事件驱动编程
前端·javascript
零陵上将军_xdr2 小时前
后端转全栈学习-Day3-JavaScript 基础-1
开发语言·javascript·学习
GISHUB2 小时前
Express + TypeScript + ESM 后端服务搭建教程
javascript·typescript·express
ZC跨境爬虫2 小时前
跟着 MDN 学CSS day_32:(Web字体深度解析与实践指南)
前端·javascript·css·ui·html
sugar__salt2 小时前
JavaScript 数组去重全解:6 种核心方法
javascript