什么是变量提升(hoisting)?var、let 和 const 在变量提升方面有什么不同?
核心答案
变量提升(Hoisting) 是 JavaScript 引擎在代码执行前的编译阶段,将变量和函数的声明提升到其所在作用域顶部的行为。
三种声明方式在变量提升方面的区别:
| 声明方式 | 是否提升 | 提升后的初始值 | 声明前能否访问 |
|---|---|---|---|
| var | ✅ 是 | undefined | ✅ 可以(值为 undefined) |
| let | ✅ 是 | 未初始化 | ❌ 不可以(TDZ 报错) |
| const | ✅ 是 | 未初始化 | ❌ 不可以(TDZ 报错) |
关键点:三者都会提升,但 let 和 const 存在暂时性死区(TDZ),在声明语句执行前访问会抛出 ReferenceError。
深入解析
1. JavaScript 代码执行的两个阶段
JavaScript 引擎执行代码分为两个阶段:
-
编译阶段(Creation Phase)
- 创建执行上下文
- 扫描所有声明(变量、函数、类)
- 在内存中分配空间(这就是"提升")
-
执行阶段(Execution Phase)
- 逐行执行代码
- 为变量赋值
2. var 的提升机制
var 声明的变量在编译阶段会:
- 声明被提升到函数作用域顶部
- 自动初始化为
undefined
// 你写的代码
console.log(x);
var x = 5;
// 引擎理解的代码(概念上)
var x = undefined; // 声明 + 初始化都提升
console.log(x); // undefined
x = 5; // 赋值留在原地
3. let/const 的提升机制与 TDZ
let 和 const 确实会提升,但与 var 不同的是:
- 声明被提升
- 不会被初始化(处于"未初始化"状态)
- 从作用域开始到声明语句之间的区域称为暂时性死区(Temporal Dead Zone, TDZ)
// TDZ 开始
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5; // TDZ 结束,x 被初始化
4. 为什么说 let/const 也会提升?
证据:如果 let 不提升,下面的代码应该打印外层的 x:
let x = 'outer';
{
console.log(x); // ReferenceError,而不是 'outer'
let x = 'inner';
}
报错说明:引擎已经知道块内有 let x 声明(提升了),所以不会去外层查找,但因为处于 TDZ 所以报错。
5. 函数提升 vs 变量提升
// 函数声明:整体提升(声明 + 函数体)
console.log(foo()); // "hello"
function foo() { return "hello"; }
// 函数表达式:只提升变量,不提升函数体
console.log(bar); // undefined
console.log(bar()); // TypeError: bar is not a function
var bar = function() { return "world"; };
6. 常见误区
❌ 误区1:"let 和 const 不会提升" ✅ 事实:它们会提升,但有 TDZ
❌ 误区2:"TDZ 是一个语法概念" ✅ 事实:TDZ 是运行时行为,与代码位置有关,而非词法位置
❌ 误区3:"在 TDZ 内变量不存在" ✅ 事实:变量已存在于作用域中,只是处于"未初始化"状态
代码示例
示例1:var 的提升
function example() {
console.log(name); // undefined(不是 ReferenceError)
console.log(age); // ReferenceError: age is not defined
var name = 'Alice';
}
示例2:TDZ 的范围
function demo() {
// TDZ 从这里开始 ↓
// 即使是 typeof 也会报错(TDZ 内)
console.log(typeof value); // ReferenceError
// TDZ 到这里结束 ↓
let value = 42;
console.log(typeof value); // "number"
}
示例3:TDZ 是运行时概念
function example() {
// 这段代码不会报错,因为 func 在 TDZ 结束后才被调用
function func() {
console.log(x); // 10
}
let x = 10;
func(); // 此时 x 已初始化
}
示例4:循环中的提升差异
// var - 只有一个变量,被共享
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出: 3, 3, 3
// let - 每次迭代都创建新的绑定
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100);
}
// 输出: 0, 1, 2
示例5:const 在 TDZ 中的行为
const x = x; // ReferenceError: Cannot access 'x' before initialization
// 等号右边的 x 在 TDZ 中,无法访问
示例6:类声明也有 TDZ
const instance = new MyClass(); // ReferenceError
class MyClass {
constructor() {
this.name = 'test';
}
}
面试技巧
可能的追问方向
-
「证明 let 会提升」
- 用上面的嵌套作用域例子说明
-
「TDZ 的设计目的是什么?」
- 让代码更可预测,在声明前使用变量通常是程序 bug
- 使
const语义更合理(避免先是 undefined 后变成其他值) - 帮助发现循环引用等问题
-
「typeof 在 TDZ 中的表现?」
- 对于未声明的变量,
typeof返回"undefined" - 对于 TDZ 中的变量,
typeof会抛出 ReferenceError
- 对于未声明的变量,
-
「函数声明和函数表达式的提升有什么区别?」
- 函数声明:整体提升
- 函数表达式:只提升变量声明部分
展示深度理解
- 提及 ECMAScript 规范中的
CreateBinding和InitializeBinding操作 - 解释执行上下文(Execution Context)和词法环境(Lexical Environment)
- 说明 V8 引擎如何在编译阶段标记变量状态
一句话总结
变量提升是 JS 引擎在编译阶段将声明"移动"到作用域顶部的行为——
var提升后初始化为 undefined 可立即访问,let/const提升后不初始化,声明前访问会触发 TDZ 报错。