JavaScript 中最令人困惑的特性之一:为什么在声明之前使用变量不会报错,而是返回
undefined?为什么函数却可以正常调用?本文带你从现象出发,逐层深入 JavaScript 引擎的内部工作机制。
目录
- 从一个反直觉的现象说起
- 变量提升是什么
- [函数提升 ------ 一等公民的待遇](#函数提升 —— 一等公民的待遇 "#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")
- [声明 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")
- 编译阶段与执行阶段
- 执行上下文:代码运行的幕后环境
- [变量环境 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")
- [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")
- 总结与最佳实践
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 词法环境
这是理解
var和let/const行为差异的关键。
7.1 变量环境 (VariableEnvironment)
- 存放
var声明的变量和函数声明 - 有变量提升 :在声明之前使用,返回
undefined - 在编译阶段完成内存分配和初始化(初始值为
undefined)
7.2 词法环境 (LexicalEnvironment)
- 存放
let和const声明的变量 - 没有变量提升 (或者说,提升但不初始化):
- 在编译阶段,变量被创建但没有初始化
- 在声明之前使用会报错 (而非返回
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. 总结与最佳实践

✅ 最佳实践
| 建议 | 说明 |
|---|---|
优先使用 let 和 const |
避免 var 的提升行为带来的困惑 |
| 先声明,后使用 | 让代码逻辑更清晰,更易维护 |
| 函数声明放在作用域顶部 | 虽然可以后置,但前置更符合阅读习惯 |
| 理解而非依赖提升 | 提升是编译机制,不应成为代码风格 |
📝 一句话记忆
变量提升发生在编译阶段,var 声明初始化为 undefined 放入变量环境;函数声明整体提升;let/const 放入词法环境且处于暂时性死区,声明前使用会报错。