JavaScript 作用域与变量提升:var、let、const 和暂时性死区
本文目录
理解变量声明,关键不是背“提升”两个字,而是判断标识符属于哪个作用域、何时可读、重新赋值会影响谁。现代代码默认使用 const,确实需要重新绑定时用 let;var 主要留给维护旧代码时识别。
函数作用域与块级作用域
var 的作用域是最近的函数或脚本范围,不受普通 if、for 花括号限制。同一作用域重复声明 var 可能不报错,容易让大型函数中的状态被意外复用。let 与 const 在 {} 块内形成词法作用域,循环每轮绑定也更符合常见预期。
声明会在代码执行前被建立,但三者可用时间不同。var 在进入作用域时初始化为 undefined,所以声明行之前读取会得到 undefined;let 和 const 在声明执行前处于暂时性死区(TDZ),提前访问会抛 ReferenceError。这比静默读到 undefined 更能暴露顺序问题。
function demonstrateScope() {
if (true) {
var legacyValue = '函数范围';
let blockValue = '块范围';
const fixedBinding = { count: 1 };
fixedBinding.count += 1; // 可改对象属性,不能重新绑定变量
}
console.log(legacyValue); // '函数范围'
// console.log(blockValue); // ReferenceError
}
const 不等于深度不可变
const 禁止重新给当前绑定赋值,但若值是对象或数组,其内部属性仍可变。需要不可变状态时,应创建新对象或数组;Object.freeze 只冻结对象自身属性,默认是浅冻结,而且不能替代业务层的状态管理约束。
默认选 const 可表达“这个名字不再指向别的值”,不是证明值永远不变。按最小作用域声明变量,必要时把值传入纯函数,减少依赖外部可变状态。
模块、严格模式与所谓隐式全局
ES module 默认运行在严格模式。对未声明标识符赋值会抛 ReferenceError,不会像某些旧式非严格脚本那样悄悄创建全局对象属性。旧文章把这种行为当作常规用法是不正确的;全局共享状态应通过模块导出、明确的依赖注入或明确挂在 globalThis 上并说明原因。
闭包表示函数保留了定义时可以访问的词法环境。它适合封装私有状态、事件回调和工厂函数,但长期保留 DOM 节点、超大对象或定时器也会延长其生命周期。卸载组件时解除监听、清除定时器,避免闭包让对象无法回收。
function createCounter() {
let count = 0;
return () => {
count += 1;
return count;
};
}
const nextCount = createCounter();
nextCount(); // 1
nextCount(); // 2:闭包持有此计数器独立的词法环境
循环回调与异步陷阱
用 var 声明循环变量并在回调中读取时,多个回调可能最终看到同一个函数级绑定。let 会为每轮循环建立独立绑定。异步代码还要关注闭包读取的是“调用时值”还是“稍后共享状态的当前值”;需要快照时显式复制原始值或构造不可变对象。
调试时在作用域面板查看 Local、Closure 和 Global,而不是只看变量名猜它来自哪里。TypeScript 类型不会改变运行时作用域规则,也不能修复竞态或副作用。
检查清单
- 是否把
var改成更小范围的const/let,同时保持行为不变? - 读取声明前变量时,是期望
undefined还是应由 TDZ 更早暴露错误? - 闭包、监听器和计时器是否在使用结束后释放?
- 对象是需要共享修改,还是应该通过新对象返回更新?
- 代码是否依赖脚本加载顺序或意外写入全局对象?
参考:MDN let、MDN const、MDN var、MDN Strict mode、MDN Closures、ECMAScript specification。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。