前言
在日常开发中,你是否遇到过这样的困惑:变量还没声明就能使用,而且不报错?函数定义在调用之后,却依然能正常执行?
这背后就是 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)
let 和 const 也会提升,但它们的行为和 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,所以声明前访问得到undefinedlet的变量提升:编译阶段也在内存中分配了空间,但没有做赋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';
六、总结
核心要点
- 变量提升发生在编译阶段,不是物理移动代码,而是 JS 引擎将声明放入内存
var声明 会被提升并赋默认值undefined,赋值留在原地- 函数声明会被整体提升(包括函数体),提前调用可正常执行
- 函数表达式只提升变量声明,不提升函数体
let/const也会提升,但存在暂时性死区(TDZ),声明前访问会报错
执行流程
源代码 → 编译阶段(生成执行上下文 + 可执行代码) → 执行阶段(按顺序执行)
最佳实践
- 优先使用
let/const,避免var的变量提升带来的混淆 - 函数调用放在声明之后,代码更易读
- 在作用域顶部集中声明变量,避免依赖提升行为
写在最后
变量提升是 JavaScript 执行机制中的基础概念,理解它不仅有助于写出更健壮的代码,也是深入学习执行上下文、作用域链、闭包等高级概念的前提。希望本文能帮你彻底搞懂这个知识点。
如果觉得有帮助,欢迎点赞收藏