变量提升(Hoisting)是 JavaScript 中最容易让新手困惑的特性之一,而 ES6 引入的 let 和 const 又带来了暂时性死区(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):let 和 const 的严谨约束
ES6 引入了块级作用域的 let 和 const,它们也会被提升,但提升行为与 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 改为 let,if 块内的 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 开发的基本准则。 - 将声明放在作用域顶部,无论是
let、const还是函数声明,这样代码的可预测性最强。 - 开启 ESLint 规则:
no-var禁止var,no-use-before-define检测声明前使用的问题,prefer-const确保不修改的变量用const。 - 理解而非死记:遇到奇异的 log 结果时,回想“创建阶段提升”和“TDZ 机制”,而不是靠猜测。
变量提升是历史遗留的设计选择,TDZ 是对这种选择的修正。掌握两者的运行机制,就能在面对作用域问题时心中有数,避免在不经意间掉进那些“明明没写错,却怎么都不对”的坑里。