深入理解 JavaScript 变量提升(Hoisting)

前言

在日常开发中,你是否遇到过这样的困惑:变量还没声明就能使用,而且不报错?函数定义在调用之后,却依然能正常执行?

这背后就是 JavaScript 的变量提升(Hoisting) 机制。本文将从现象入手,带你深入理解 JS 引擎的编译阶段和执行阶段,彻底搞懂变量提升的本质。


一、从一个奇怪的现象说起

先看一段代码:

javascript 复制代码
showName();
console.log(myName);
var myName = 'zhangsan';
function showName() {
    console.log('函数showName被执行了');
}

执行结果:

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

按照"代码逐行执行"的直觉,showName()console.log(myName) 应该报错才对,因为它们用到的变量和函数在后面才声明。但实际却没有报错,这是为什么?

三种情况总结

情况 是否报错 结果
使用未声明的变量 ✅ 报错 ReferenceError
var 声明前使用变量 ❌ 不报错 undefined
在函数声明前调用函数 ❌ 不报错 正常执行

这说明 JS 代码并不是一行一行执行的 ,在执行之前还有一个编译阶段


二、什么是变量提升

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

1. 变量提升的本质

很多人以为变量提升是把代码"物理层面"移动到最前面,这个理解并不准确

实际上:

  • 变量和函数声明在代码中的位置不会改变
  • 而是在编译阶段,JS 引擎将其放入内存中
  • 为执行阶段做好准备

2. 拆解 var myName = 'zhangsan'

这行代码其实包含两部分:

javascript 复制代码
var myName;           // 声明部分(会被提升)
myName = 'zhangsan';       // 赋值部分(不会被提升)
  • 声明 :在编译阶段被提升,赋默认值 undefined
  • 赋值:留在原地,等执行阶段才运行

3. 函数声明 vs 函数表达式

javascript 复制代码
// 函数声明 ------ 整体被提升(包括函数体)
function foo() {
    console.log('foo');
}

// 函数表达式 ------ 只提升变量声明,不提升函数体
var bar = function() {
    console.log('bar');
}

关键区别:

  • function 声明的函数,整个函数体都会被提升,所以提前调用能正常执行
  • var 声明的函数表达式,只提升变量名,值为 undefined,提前调用会报 TypeError

三、JS 代码的执行流程

要真正理解变量提升,需要了解 JS 代码的两个阶段:编译阶段执行阶段

JS 是脚本语言、弱动态类型语言,它没有独立的编译阶段,编译就发生在运行前的那一刹那

1. 编译阶段

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

markdown 复制代码
代码 → 编译 → ┌─ 执行上下文对象(Execution Context)
              └─ 可执行代码

执行上下文是 JS 执行一段代码的运行环境,包含:

  • 变量环境(Variable Environment) :存放 var 声明的变量和函数声明
  • 词法环境(Lexical Environment) :存放 let/const 声明的变量

以这段代码为例:

javascript 复制代码
showName();
console.log(myname);
var myname = 'zhangsan';
function showName() {
    console.log('函数showName被执行了');
}

编译后,执行上下文的变量环境为:

javascript 复制代码
VariableEnvironment:
    myname  -> undefined          // var 声明,默认值 undefined
    showName -> function() {...}  // 函数声明,完整提升

可执行代码为:

javascript 复制代码
showName();
console.log(myname);
myname = 'zhangsan';

2. 执行阶段

编译完成后,JS 引擎按顺序执行"可执行代码":

javascript 复制代码
// 第1步:调用 showName,此时函数已存在 → 正常执行
showName();                          // 输出:函数showName被执行了

// 第2步:打印 myname,此时值为 undefined
console.log(myname);                 // 输出:undefined

// 第3步:赋值,此时 myname 才有真正的值
myname = 'zhangsan';

3. 完整流程图

复制代码
源代码
  │
  ▼
编译阶段 ──→ 生成执行上下文(变量环境 + 词法环境)
  │         生成可执行代码
  ▼
执行阶段 ──→ 按顺序执行可执行代码

四、let/const 与暂时性死区(TDZ)

letconst 也会提升,但它们的行为和 var 不同。

javascript 复制代码
console.log(myname); // ReferenceError: Cannot access 'myname' before initialization
let myname = 'zhangsan';

为什么会报错?

  • let/const 声明的变量存放在词法环境
  • 在声明之前,这些变量处于暂时性死区(Temporal Dead Zone, TDZ)
  • 处于 TDZ 中的变量不能被访问 ,否则抛出 ReferenceError

var vs let 对比

javascript 复制代码
// var ------ 变量提升,值为 undefined
console.log(a);  // undefined
var a = 1;

// let ------ 暂时性死区,报错
console.log(b);  // ReferenceError
let b = 1;

本质区别:

  • var 的变量提升:编译阶段完成内存分配,并赋默认值 undefined,所以声明前访问得到 undefined
  • let 的变量提升:编译阶段也在内存中分配了空间,但没有做赋 undefined 的操作,且被标记为 TDZ,声明前访问直接报错

五、模拟变量提升的过程

理解了原理,我们可以手动模拟变量提升的过程。原始代码:

javascript 复制代码
showName();
console.log(myname);
var myname = 'zhangsan';
function showName() {
    console.log('函数showName被执行了');
}

模拟编译阶段(变量提升部分):

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

模拟执行阶段(可执行代码):

javascript 复制代码
showName();
console.log(myname);
myname = 'zhangsan';

合并起来就是 JS 引擎实际处理后的等价代码:

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

六、总结

核心要点

  1. 变量提升发生在编译阶段,不是物理移动代码,而是 JS 引擎将声明放入内存
  2. var 声明 会被提升并赋默认值 undefined赋值留在原地
  3. 函数声明会被整体提升(包括函数体),提前调用可正常执行
  4. 函数表达式只提升变量声明,不提升函数体
  5. let/const 也会提升,但存在暂时性死区(TDZ),声明前访问会报错

执行流程

复制代码
源代码 → 编译阶段(生成执行上下文 + 可执行代码) → 执行阶段(按顺序执行)

最佳实践

  • 优先使用 let/const,避免 var 的变量提升带来的混淆
  • 函数调用放在声明之后,代码更易读
  • 在作用域顶部集中声明变量,避免依赖提升行为

写在最后

变量提升是 JavaScript 执行机制中的基础概念,理解它不仅有助于写出更健壮的代码,也是深入学习执行上下文、作用域链、闭包等高级概念的前提。希望本文能帮你彻底搞懂这个知识点。

如果觉得有帮助,欢迎点赞收藏

相关推荐
大猫会长1 小时前
获取favicon.ico的方法
前端·javascript·html
HeiSenBerg1 小时前
Android LiveData 原理完整剖析:从订阅、分发到粘性事件
面试
默_笙1 小时前
🎀 大模型说"呀"还是"吗"?temperature 和 Top K 在偷偷控制它的"性格"
前端·javascript
为你学会写情书1 小时前
为什么弹窗状态不该放在全局Store?React性能优化面经复盘与实战指南
面试
乌夷1 小时前
vue知识点
前端·javascript·vue.js
众人皆醒我独醉1 小时前
什么是输入上下文、MCP、Agent、Skills?—— 四个让你用不好 AI 的概念,一次讲清楚
面试·ai编程
blns_yxl2 小时前
拖拽排序列表(HTML+JS)
前端·javascript·html
Larcher2 小时前
别再搞混了!一文讲透 AI Workflow 和 Agent 的本质区别
javascript·人工智能·后端
daols882 小时前
vue vxe-context-menu 通用右键菜单组件使用
前端·javascript·vue.js·vxeui