JavaScript 中最令人困惑的特性之一:为什么在声明之前使用变量不会报错,而是返回
undefined?为什么函数却可以正常调用?本文带你从现象出发,逐层深入 JavaScript 引擎的内部工作机制。
目录
- 从一个反直觉的现象说起
- 变量提升是什么
- 函数提升 —— 一等公民的待遇
- 声明 vs 赋值:拆开来看
- 编译阶段与执行阶段
- 执行上下文:代码运行的幕后环境
- 变量环境 vs 词法环境
- let / const 与暂时性死区
- 总结与最佳实践
1. 从一个反直觉的现象说起
请看下面这段代码,猜猜它会输出什么?
showName();
console.log(myName);
var myName = '极客时间';
function showName() {
console.log('函数 showName 被执行了');
}
直觉告诉我们:
- 第 1 行:
showName()—— 函数还没定义呢,应该报错! - 第 2 行:
console.log(myName)—— 变量还没声明呢,也应该报错!
然而实际运行结果是:
函数 showName 被执行了
undefined
函数正常执行了,变量也没有报错,只是值为 undefined。为什么?
┌─────────────────────────────────────────────────────────────────┐
│ 直觉预期 vs 实际结果 │
├─────────────────────┬───────────────────┬───────────────────────┤
│ 代码 │ 直觉预期 │ 实际结果 │
├─────────────────────┼───────────────────┼───────────────────────┤
│ showName() │ ❌ 报错 │ ✅ 正常执行 │
│ console.log(myName) │ ❌ 报错 │ ⚠️ undefined │
└─────────────────────┴───────────────────┴───────────────────────┘
这说明了一个重要事实:JavaScript 代码并不是一行一行直接执行的。
2. 变量提升是什么
2.1 定义
变量提升(Hoisting) 是指在 JavaScript 代码执行过程中,JavaScript 引擎(如 Chrome V8)把变量声明部分和函数声明部分提前到作用域顶部进行处理的行为。变量被提升后,会被赋予默认值
undefined。
2.2 一个常见的误解
很多人以为"变量提升"就是把代码在物理层面移动到最前面。比如:
// 原始代码
console.log(myName);
var myName = '极客时间';
被误解为:
// ❌ 错误的理解:物理移动
var myName;
console.log(myName);
myName = '极客时间';
实际上,变量和函数声明在代码中的位置从未改变。 真正发生的是:在编译阶段,JavaScript 引擎将这些声明信息放入内存之中,为执行阶段做好准备。
2.3 图解变量提升的本质
3. 函数提升 —— 一等公民的待遇
3.1 函数声明的提升
函数声明会被整体提升——不仅提升了声明,连函数体也一起提升了:
// 可以在声明之前调用!
foo(); // 输出:'foo'
function foo() {
console.log('foo');
}
编译阶段的内存模型:
┌─────────────────────────────────┐
│ VariableEnvironment │
│ ┌───────────────────────────┐ │
│ │ foo → function { │ │
│ │ console.log('foo') │ │
│ │ } │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
3.2 函数表达式 vs 函数声明
这是初学者最容易犯错的地方。函数表达式不会整体提升:
// ❌ 函数表达式 —— 报错!
bar(); // TypeError: bar is not a function
var bar = function() {
console.log('bar');
};
为什么?因为 var bar 被提升了,但赋值 = function() {...} 没有。编译后等效于:
var bar = undefined; // 编译阶段:声明提升,值为 undefined
bar(); // 执行阶段:bar 是 undefined,无法调用!报错
bar = function() { // 执行阶段:赋值
console.log('bar');
};
3.3 函数 vs 变量 —— 谁的优先级更高?
当函数和变量同名时,函数提升优先于变量提升:
console.log(typeof foo); // "function"
var foo = 'hello';
function foo() {
return 'world';
}
4. 声明 vs 赋值:拆开来看
理解变量提升的关键,是把一行代码拆成两部分:
var myName = '极客时间';
│ │
│ └── 赋值部分(执行阶段处理)
│
└── 声明部分(编译阶段处理)
我们可以用代码来模拟变量提升后代码的逻辑形态:
编译阶段产物 —— 声明部分(4.js)
// 编译阶段:变量和函数声明被放入内存
var myname = undefined;
function showName() {
console.log('showName 被执行了');
}
执行阶段产物 —— 可执行代码(5.js)
// 执行阶段:按顺序逐行执行
showName(); // → 'showName 被执行了'
console.log(myname); // → undefined
myname = '极客时间'; // → 赋值
完整模拟效果(3.js):
// 把编译阶段的声明和执行阶段的代码组合起来看
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)。
// ─── TDZ 开始 ───
console.log(myname); // ❌ ReferenceError: Cannot access 'myname' before initialization
// ─── TDZ 结束 ───
let myname = '极客时间';
// 可以正常使用了
console.log(myname); // ✅ '极客时间'
8.2 var 提升 vs let 不提升
// 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 放入词法环境且处于暂时性死区,声明前使用会报错。