题目重点:用循环闭包和暂时性死区两个小例子说明 var/let 差异,避免把变量提升误说成 let 没有声明。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 使用现代 JavaScript 模块的严格模式;`var` 在函数作用域,`let` 在块作用域,循环头的 let 每次迭代形成新绑定。
- 题目未明确任何输出题代码,因此本文不推断面试官使用了哪个闭包或 TDZ 片段。
解题路线
- 先分声明范围:var 受函数边界约束,let 受最近 `{}` 块约束;var 重复声明通常允许,let 同一作用域重复声明是语法错误。
- 再看初始化时点:var 声明会提升且初始化为 undefined;let 声明也参与作用域解析,但从作用域开始到声明执行前为 TDZ,提前读会抛 ReferenceError。
- 循环闭包中 `for(var i...)` 三个函数共享同一 i,调用时都看结束后的 3;`for(let j...)` 每轮产生新绑定,结果 0、1、2。不要将这个行为错误归因于异步或线程。
手动走一遍
训练函数返回 varResults=[3,3,3]、letResults=[0,1,2];块内在 `let value` 声明前读取 value,捕获 ReferenceError。若将回调改成立即调用,var 数列也会是 0、1、2,说明关键在调用时点和绑定是否共享。
JavaScript 参考实现
下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。
代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。
export function compareVarLet() {
const fromVar = [];
const fromLet = [];
for (var i = 0; i < 3; i++) fromVar.push(() => i);
for (let j = 0; j < 3; j++) fromLet.push(() => j);
let tdzError;
try {
{
void value;
let value = 1;
}
} catch (error) {
tdzError = error.name;
}
return { varResults: fromVar.map((f) => f()), letResults: fromLet.map((f) => f()), tdzError };
}
复杂度
示例创建 N 个闭包占 O(N) 空间,求值 O(N);var/let 选择主要影响正确性与可维护性,并非可依赖的性能优化。
易错点与追问
容易失分的边界
- 说 let 完全不提升,因此声明前会读到外层同名变量。
- 把 var 与 let 的区别只归为是否可改值,混淆 const。
- 忽略闭包在循环结束后才调用。
- 以为 TDZ 错误仅在赋值时发生,读取同样会触发。
继续深挖
- 块内 let 与外层同名变量时,声明前读取哪一个?
- 把 var 闭包放在 setTimeout 中输出如何变化?
- const 与 let 的绑定和对象可变性差别是什么?
