人人都会AI编程

32.1 变量提升与暂时性死区坑点

更新时间:2026-07-11

变量提升(Hoisting)是 JavaScript 中最容易让新手困惑的特性之一,而 ES6 引入的 letconst 又带来了暂时性死区(Temporal Dead Zone,简称 TDZ)这一新概念。这两者结合,在实际编码中制造了不少隐蔽的坑。理解它们的本质和表现,是写出可靠 JavaScript 代码的基本功。

变量提升的本质

所谓“提升”,并不是代码被物理移动到了文件顶部,而是 JavaScript 引擎在执行上下文创建阶段,会先扫描当前作用域内的所有变量和函数声明,将它们添加到当前执行上下文的词法环境中。

对于 var 声明的变量

var 声明的变量在提升时会被初始化为 undefined

console.log(a);  // undefined,而不是报错
var a = 10;
console.log(a);  // 10

实际执行顺序相当于:

var a;           // 提升,初始化为 undefined
console.log(a);  // undefined
a = 10;          // 赋值留在原地
console.log(a);  // 10

对于函数声明

函数声明也会被提升,而且整个函数体都会被一同提升:

sayHello();  // "Hello",不会报错

function sayHello() {
  console.log("Hello");
}

而函数表达式(包括箭头函数赋值给变量)则按照变量提升规则处理,只有变量名被提升,函数体留在原地:

greet();  // TypeError: greet is not a function
var greet = function() {
  console.log("Hi");
};
// 相当于 var greet = undefined; greet() 自然报错

这种差异很容易让人写出“函数在定义前调用居然不报错”的代码,对代码可读性并不友好。

暂时性死区(TDZ):letconst 的严谨约束

ES6 引入了块级作用域的 letconst,它们也会被提升,但提升行为与 var 截然不同。在声明语句执行之前,变量处于不可访问的暂时性死区,任何访问操作都会导致 ReferenceError,即使只是在赋值前使用 typeof 也会报错。

console.log(b);  // ReferenceError: Cannot access 'b' before initialization
let b = 20;

// 即使在 TDZ 内使用 typeof 也不行
{
  typeof c;  // ReferenceError(如果后面有 let/const c)
  let c;
}

为什么要有 TDZ?这是为了让开发者避免使用未初始化的变量,强制遵循“先声明后使用”的规范,减少因隐式 undefined 值导致的逻辑错误。

现实中最容易踩的三个坑

坑一:跨块作用域的变量覆盖

var name = "全局";

function show() {
  console.log(name);  // 你以为打印 "全局",实际是 undefined
  if (false) {
    var name = "局部";  // var 无视块作用域,提升到函数顶部
  }
}
show();

由于 var 是函数级作用域,if 块内部的 var name 会被提升到 show 函数顶部,并初始化为 undefined,导致外部 name 被遮蔽。如果将 var 改为 letif 块内的 name 仅存在于该块内,不会影响外层,代码表现会更符合直觉。

坑二:循环内异步访问索引变量

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);  // 打印三次 3,而不是 0,1,2
}

var i 是函数级作用域,整个循环共用一个 i。当定时器回调执行时,循环早已结束,i 已经变成了 3。解决方式:将 var 改为 let(产生块级作用域,每次迭代都绑定一个新的 i),或者使用闭包手动绑定值。这是经典面试题,也是异步逻辑中最常见的提升相关坑。

坑三:typeof 安全检查失效

很多开发者习惯用 typeof 判断一个变量是否存在,以避免 ReferenceError。但 TDZ 下连 typeof 都会报错:

{
  typeof x; // ReferenceError  (后面有 let x)
  let x;
}

如果你的代码中使用了 typeof 做全局变量环境检测(如 typeof global !== 'undefined'),需要注意不能将其放在该变量可能处于 TDZ 的地方。通常这种检测应放在全局作用域开头,且在所有 let/const 声明之前。

如何避开这些坑

  • 默认使用 const,需要修改时用 let,不再使用 var。这是现代 JavaScript 开发的基本准则。
  • 将声明放在作用域顶部,无论是 letconst 还是函数声明,这样代码的可预测性最强。
  • 开启 ESLint 规则no-var 禁止 varno-use-before-define 检测声明前使用的问题,prefer-const 确保不修改的变量用 const
  • 理解而非死记:遇到奇异的 log 结果时,回想“创建阶段提升”和“TDZ 机制”,而不是靠猜测。

变量提升是历史遗留的设计选择,TDZ 是对这种选择的修正。掌握两者的运行机制,就能在面对作用域问题时心中有数,避免在不经意间掉进那些“明明没写错,却怎么都不对”的坑里。