讲解

作用域决定变量在哪里可见。JavaScript 有三层作用域:全局作用域(脚本任何地方可访问)、函数作用域(var 的唯一作用域单位)、块级作用域(let/const 的作用域单位,任何一对 {} 都是一个块:if、for、while 的花括号都算)。查找变量时引擎从当前作用域逐层向外找,直到全局,找不到就抛 ReferenceError——这条链叫作用域链。内层可以访问外层,外层访问不到内层。

提升(hoisting)是指声明在编译阶段被「搬」到作用域顶部的行为,但三种声明的提升方式不同:var 的声明提升且初始化为 undefined,所以声明前访问得到 undefined(不报错但常常是 bug);let/const 的声明提升但不初始化,声明前访问抛 ReferenceError——从块开始到声明语句之间的这段区域叫「暂时性死区」(TDZ);函数声明则整体提升(函数体也提升),声明前就能调用。

理解这套规则能解释大量「灵异现象」。例如经典的循环 var 陷阱:for (var i...) 里的 setTimeout 回调全部读到最终的 i,因为 var 只有一个共享绑定;改成 let 后每轮迭代创建新绑定,各回调读到各自的 i。工程结论很简单:用 let/const,忘掉 var,提升相关的坑就消失了 90%。

示例

// var:声明提升并初始化 undefined
console.log('var 提前访问:', typeof x); // 'undefined',不报错
var x = 1;

// let:暂时性死区,提前访问报错
try {
  console.log(y);
  let y = 2;
} catch (e) {
  console.log('TDZ 报错:', e.constructor.name); // ReferenceError
}

// 块级作用域:let/const 只在 {} 内有效
{
  const inner = '块内变量';
  console.log('块内访问:', inner);
}
try {
  console.log(inner);
} catch (e) {
  console.log('块外访问:', e.constructor.name);
}

// 作用域链:内层找外层
const globalVar = '全局';
function outer() {
  const outerVar = '函数内';
  if (true) {
    console.log('内层能读到:', globalVar, outerVar);
  }
}
outer();

// var 循环陷阱的 let 解法(同步模拟回调收集)
const fns = [];
for (let i = 0; i < 3; i++) {
  fns.push(() => i);
}
console.log('let 各存各的:', fns.map((f) => f())); // [0, 1, 2]

常见坑

  • 误以为 if/for 的花括号不构成作用域:var 无视块级作用域会泄漏到外层,let/const 则被块包住。在块里声明、块外使用的代码从 var 迁移到 let 时会报未定义。
  • 在 TDZ 里用 typeof:老说法「typeof 不存在的变量返回 undefined 不报错」对 let/const 的 TDZ 失效——typeof y(y 在 TDZ 中)照样抛 ReferenceError。
  • 函数表达式的提升误解:const f = function(){} 提升的只是变量声明(处于 TDZ),函数体不提升,提前调用报错。
  • 重复声明混用:同一作用域 var a; let a; 直接语法错误,大文件迁移时注意清理。

小结

作用域分全局/函数/块级,变量沿作用域链向外查找。var 提升为 undefined,let/const 有暂时性死区,函数声明整体提升。下一节讲由作用域链支撑的强大机制:闭包。