深入理解 JavaScript 变量提升(Hoisting)—— 从现象到原理

JavaScript 中最令人困惑的特性之一:为什么在声明之前使用变量不会报错,而是返回 undefined?为什么函数却可以正常调用?本文带你从现象出发,逐层深入 JavaScript 引擎的内部工作机制。


目录

  1. 从一个反直觉的现象说起
  2. 变量提升是什么
  3. [函数提升 ------ 一等公民的待遇](#函数提升 —— 一等公民的待遇 "#3-%E5%87%BD%E6%95%B0%E6%8F%90%E5%8D%87--%E4%B8%80%E7%AD%89%E5%85%AC%E6%B0%91%E7%9A%84%E5%BE%85%E9%81%87")
  4. [声明 vs 赋值:拆开来看](#声明 vs 赋值:拆开来看 "#4-%E5%A3%B0%E6%98%8E-vs-%E8%B5%8B%E5%80%BC%E6%8B%86%E5%BC%80%E6%9D%A5%E7%9C%8B")
  5. 编译阶段与执行阶段
  6. 执行上下文:代码运行的幕后环境
  7. [变量环境 vs 词法环境](#变量环境 vs 词法环境 "#7-%E5%8F%98%E9%87%8F%E7%8E%AF%E5%A2%83-vs-%E8%AF%8D%E6%B3%95%E7%8E%AF%E5%A2%83")
  8. [let / const 与暂时性死区](#let / const 与暂时性死区 "#8-let--const-%E4%B8%8E%E6%9A%82%E6%97%B6%E6%80%A7%E6%AD%BB%E5%8C%BA")
  9. 总结与最佳实践

1. 从一个反直觉的现象说起

请看下面这段代码,猜猜它会输出什么?

javascript 复制代码
showName();
console.log(myName);
var myName = '极客时间';

function showName() {
    console.log('函数 showName 被执行了');
}

直觉告诉我们:

  • 第 1 行:showName() ------ 函数还没定义呢,应该报错!
  • 第 2 行:console.log(myName) ------ 变量还没声明呢,也应该报错!

然而实际运行结果是:

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

函数正常执行了,变量也没有报错,只是值为 undefined。为什么?

scss 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    直觉预期 vs 实际结果                          │
├─────────────────────┬───────────────────┬───────────────────────┤
│        代码          │     直觉预期      │      实际结果         │
├─────────────────────┼───────────────────┼───────────────────────┤
│ showName()          │    ❌ 报错        │   ✅ 正常执行         │
│ console.log(myName) │    ❌ 报错        │   ⚠️ undefined       │
└─────────────────────┴───────────────────┴───────────────────────┘

这说明了一个重要事实:JavaScript 代码并不是一行一行直接执行的。


2. 变量提升是什么

2.1 定义

变量提升(Hoisting) 是指在 JavaScript 代码执行过程中,JavaScript 引擎(如 Chrome V8)把变量声明部分函数声明部分 提前到作用域顶部进行处理的行为。变量被提升后,会被赋予默认值 undefined

2.2 一个常见的误解

很多人以为"变量提升"就是把代码在物理层面移动到最前面。比如:

javascript 复制代码
// 原始代码
console.log(myName);
var myName = '极客时间';

被误解为:

javascript 复制代码
// ❌ 错误的理解:物理移动
var myName;
console.log(myName);
myName = '极客时间';

实际上,变量和函数声明在代码中的位置从未改变。 真正发生的是:在编译阶段,JavaScript 引擎将这些声明信息放入内存之中,为执行阶段做好准备。

2.3 图解变量提升的本质


3. 函数提升 ------ 一等公民的待遇

3.1 函数声明的提升

函数声明会被整体提升------不仅提升了声明,连函数体也一起提升了:

javascript 复制代码
// 可以在声明之前调用!
foo(); // 输出:'foo'

function foo() {
    console.log('foo');
}

编译阶段的内存模型:

javascript 复制代码
┌─────────────────────────────────┐
│      VariableEnvironment        │
│  ┌───────────────────────────┐  │
│  │  foo → function {         │  │
│  │    console.log('foo')     │  │
│  │  }                        │  │
│  └───────────────────────────┘  │
└─────────────────────────────────┘

3.2 函数表达式 vs 函数声明

这是初学者最容易犯错的地方。函数表达式不会整体提升:

javascript 复制代码
// ❌ 函数表达式 ------ 报错!
bar(); // TypeError: bar is not a function

var bar = function() {
    console.log('bar');
};

为什么?因为 var bar 被提升了,但赋值 = function() {...} 没有。编译后等效于:

javascript 复制代码
var bar = undefined;    // 编译阶段:声明提升,值为 undefined

bar();                  // 执行阶段:bar 是 undefined,无法调用!报错

bar = function() {      // 执行阶段:赋值
    console.log('bar');
};

3.3 函数 vs 变量 ------ 谁的优先级更高?

当函数和变量同名时,函数提升优先于变量提升

javascript 复制代码
console.log(typeof foo); // "function"

var foo = 'hello';

function foo() {
    return 'world';
}

4. 声明 vs 赋值:拆开来看

理解变量提升的关键,是把一行代码拆成两部分:

ini 复制代码
     var myName = '极客时间';
     │          │
     │          └── 赋值部分(执行阶段处理)
     │
     └── 声明部分(编译阶段处理)

我们可以用代码来模拟变量提升后代码的逻辑形态:

编译阶段产物 ------ 声明部分(4.js)

javascript 复制代码
// 编译阶段:变量和函数声明被放入内存
var myname = undefined;

function showName() {
    console.log('showName 被执行了');
}

执行阶段产物 ------ 可执行代码(5.js)

javascript 复制代码
// 执行阶段:按顺序逐行执行
showName();            // → 'showName 被执行了'
console.log(myname);   // → undefined
myname = '极客时间';   // → 赋值

完整模拟效果(3.js):

javascript 复制代码
// 把编译阶段的声明和执行阶段的代码组合起来看
var myname = undefined;
function showName() {
    console.log('showName 被执行了');
}
// ─────── 以上是编译阶段准备 ───────
// ─────── 以下是执行阶段运行 ───────
showName();
console.log(myname);
myname = '极客时间';

5. 编译阶段与执行阶段

5.1 JavaScript 没有独立的编译阶段?

JavaScript 是脚本语言弱类型动态 的,它不像 Java/C++ 那样有一个独立的、耗时的编译过程。但它在代码运行前的一瞬间会进行编译。

复制代码
┌──────────────────────────────────────────────────────┐
│                   JS 代码执行全流程                    │
│                                                      │
│   源代码                                               │
│     │                                                │
│     ▼                                                │
│   ┌──────────┐      ┌─────────────────┐              │
│   │  编译阶段  │ ──► │ 执行上下文       │              │
│   │ (一瞬间) │      │ + 可执行代码     │              │
│   └──────────┘      └─────────┬───────┘              │
│                               │                      │
│                               ▼                      │
│                          ┌──────────┐                │
│                          │  执行阶段  │               │
│                          │ (逐行执行)│               │
│                          └──────────┘                │
│                                                      │
└──────────────────────────────────────────────────────┘

5.2 编译阶段做了什么

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

产物 说明
执行上下文 (Execution Context) 代码运行的环境,包含变量环境、词法环境、作用域链等
可执行代码 编译后的字节码,供引擎逐条执行

6. 执行上下文:代码运行的幕后环境

6.1 什么是执行上下文

执行上下文是 JavaScript 执行一段代码时的运行环境 。每当你调用一个函数,JavaScript 引擎就会创建一个新的执行上下文,并将其压入调用栈

6.2 执行上下文内部结构


7. 变量环境 vs 词法环境

这是理解 varlet/const 行为差异的关键。

7.1 变量环境 (VariableEnvironment)

  • 存放 var 声明的变量和函数声明
  • 有变量提升 :在声明之前使用,返回 undefined
  • 在编译阶段完成内存分配和初始化(初始值为 undefined

7.2 词法环境 (LexicalEnvironment)

  • 存放 letconst 声明的变量
  • 没有变量提升 (或者说,提升但不初始化):
    • 在编译阶段,变量被创建但没有初始化
    • 在声明之前使用会报错 (而非返回 undefined

8. let / const 与暂时性死区

8.1 什么是暂时性死区

let/const 声明的变量在词法环境中,从代码块开始到变量声明之前的这段区域,称为暂时性死区(Temporal Dead Zone,TDZ)

javascript 复制代码
// ─── TDZ 开始 ───
console.log(myname);  // ❌ ReferenceError: Cannot access 'myname' before initialization
// ─── TDZ 结束 ───
let myname = '极客时间';
// 可以正常使用了
console.log(myname);  // ✅ '极客时间'

8.2 var 提升 vs let 不提升

javascript 复制代码
// var ------ 在变量环境中,声明前可用(值为 undefined)
console.log(a); // undefined
var a = 1;

// let ------ 在词法环境中,声明前不可用(暂时性死区)
console.log(b); // ReferenceError!
let b = 2;

8.3 本质

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

  • var 声明 :编译阶段 → 分配内存 + 初始化为 undefined → 变量环境(VariableEnvironment)
  • let/const 声明 :编译阶段 → 分配内存但不初始化 → 词法环境(LexicalEnvironment)→ 声明前使用 → 暂时性死区 → 报错

9. 总结与最佳实践

✅ 最佳实践

建议 说明
优先使用 letconst 避免 var 的提升行为带来的困惑
先声明,后使用 让代码逻辑更清晰,更易维护
函数声明放在作用域顶部 虽然可以后置,但前置更符合阅读习惯
理解而非依赖提升 提升是编译机制,不应成为代码风格

📝 一句话记忆

变量提升发生在编译阶段,var 声明初始化为 undefined 放入变量环境;函数声明整体提升;let/const 放入词法环境且处于暂时性死区,声明前使用会报错。


相关推荐
陈_杨1 小时前
鸿蒙APP开发-带你开发锻艺册APP的材料清单功能
前端·javascript
xixixin_1 小时前
Promise.all 和 Promise.allSettled 详解
前端·javascript·vue.js
暗冰ཏོ1 小时前
前端数据大屏开发完整指南:Vue3 + ECharts 自适应可视化实战
前端·javascript·echarts·数据大屏·大屏端
陈_杨1 小时前
鸿蒙APP开发-带你了解单块酷APP参数管理的功能
前端·javascript
怕浪猫2 小时前
Electron 开发实战(八):多媒体处理全解|音视频播放、录屏、FFmpeg 实战
前端·javascript·electron
kyriewen112 小时前
开源|Image Harvest v1.0.5:AI 智能标签 + Eagle 导出,设计师和开发者的图片工作流神器
前端·javascript·人工智能
步十人2 小时前
【Vue】认识单文件组件与模板语法
前端·javascript·vue.js
shuoshuohaohao2 小时前
《JavaScript》
开发语言·前端·javascript
编程猪猪侠2 小时前
基于uni-app-x 与 uni-app 的安卓与 H5 双向通信完整实现
android·javascript·uni-app