什么是变量提升(hoisting)?var、let 和 const 在变量提升方面有什么不同?

6 阅读4分钟

什么是变量提升(hoisting)?var、let 和 const 在变量提升方面有什么不同?

核心答案

变量提升(Hoisting) 是 JavaScript 引擎在代码执行前的编译阶段,将变量和函数的声明提升到其所在作用域顶部的行为。

三种声明方式在变量提升方面的区别:

声明方式是否提升提升后的初始值声明前能否访问
var✅ 是undefined✅ 可以(值为 undefined)
let✅ 是未初始化❌ 不可以(TDZ 报错)
const✅ 是未初始化❌ 不可以(TDZ 报错)

关键点:三者都会提升,但 letconst 存在暂时性死区(TDZ),在声明语句执行前访问会抛出 ReferenceError


深入解析

1. JavaScript 代码执行的两个阶段

JavaScript 引擎执行代码分为两个阶段:

  1. 编译阶段(Creation Phase)

    • 创建执行上下文
    • 扫描所有声明(变量、函数、类)
    • 在内存中分配空间(这就是"提升")
  2. 执行阶段(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

letconst 确实会提升,但与 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';
  }
}

面试技巧

可能的追问方向

  1. 「证明 let 会提升」

    • 用上面的嵌套作用域例子说明
  2. 「TDZ 的设计目的是什么?」

    • 让代码更可预测,在声明前使用变量通常是程序 bug
    • 使 const 语义更合理(避免先是 undefined 后变成其他值)
    • 帮助发现循环引用等问题
  3. 「typeof 在 TDZ 中的表现?」

    • 对于未声明的变量,typeof 返回 "undefined"
    • 对于 TDZ 中的变量,typeof 会抛出 ReferenceError
  4. 「函数声明和函数表达式的提升有什么区别?」

    • 函数声明:整体提升
    • 函数表达式:只提升变量声明部分

展示深度理解

  • 提及 ECMAScript 规范中的 CreateBindingInitializeBinding 操作
  • 解释执行上下文(Execution Context)和词法环境(Lexical Environment)
  • 说明 V8 引擎如何在编译阶段标记变量状态

一句话总结

变量提升是 JS 引擎在编译阶段将声明"移动"到作用域顶部的行为——var 提升后初始化为 undefined 可立即访问,let/const 提升后不初始化,声明前访问会触发 TDZ 报错。