讲解
作用域决定变量在哪里可见。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 有暂时性死区,函数声明整体提升。下一节讲由作用域链支撑的强大机制:闭包。