Full Stack Intern · Frontend Onsite

Rippling 前端:var 与 let 的作用域、TDZ 和循环闭包

用循环闭包和暂时性死区两个小例子说明 var/let 差异,避免把变量提升误说成 let 没有声明。

题目重点:用循环闭包和暂时性死区两个小例子说明 var/let 差异,避免把变量提升误说成 let 没有声明。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 使用现代 JavaScript 模块的严格模式;`var` 在函数作用域,`let` 在块作用域,循环头的 let 每次迭代形成新绑定。
  • 题目未明确任何输出题代码,因此本文不推断面试官使用了哪个闭包或 TDZ 片段。

解题路线

  1. 先分声明范围:var 受函数边界约束,let 受最近 `{}` 块约束;var 重复声明通常允许,let 同一作用域重复声明是语法错误。
  2. 再看初始化时点:var 声明会提升且初始化为 undefined;let 声明也参与作用域解析,但从作用域开始到声明执行前为 TDZ,提前读会抛 ReferenceError。
  3. 循环闭包中 `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 的绑定和对象可变性差别是什么?
✓已复制微信号!