JavaScript 暂时性死区:外层已有同名变量,typeof 为什么仍然报错

前天 3阅读

把几行代码包进一个块,并在块末尾声明局部变量后,原先用于检查外层配置的 typeof 却突然抛错。原因不是 typeof 失去类型判断能力,而是同名的局部绑定已经影响整个块,执行尚未走到初始化时,它还不能被读取。

存在一个名字,不代表已经有可读取的值

let 和 const 的绑定从所在作用域开始就参与名字解析,但要等对应初始化完成后才能读取。这段不可读取的执行阶段通常叫暂时性死区。它和“已经初始化为 undefined”不同;前者读取会抛出 ReferenceError,后者则是一个正常的值。

下面保存为 demo.mjs,再运行 node demo.mjs。独立模块便于避开控制台多次输入可能留下的同名变量。所有预期错误都放进断言函数里,整个程序应正常退出,既验证失败边界,也能继续检查初始化后的行为。

import assert from "node:assert/strict";

assert.equal(typeof missingNameForTDZDemo, "undefined");
console.log("unresolved typeof:", typeof missingNameForTDZDemo);

const mode = "outer";
{
    assert.throws(() => mode, ReferenceError);
    assert.throws(() => typeof mode, ReferenceError);
    let mode = "inner";
    assert.equal(mode, "inner");
    console.log("initialized local:", mode);
}
assert.equal(mode, "outer");
console.log("outer preserved:", mode);

{
    assert.throws(() => typeof pending, ReferenceError);
    let pending;
    assert.equal(pending, undefined);
    assert.equal(typeof pending, "undefined");
    console.log("declared without initializer:", typeof pending);
}

const seed = 7;
assert.throws(() => {
    let seed = seed;
    return seed;
}, ReferenceError);
{
    const localSeed = seed + 1;
    assert.equal(localSeed, 8);
    assert.equal(seed, 7);
}

{
    const reader = () => ready;
    assert.throws(() => reader(), ReferenceError);
    let ready = 42;
    assert.equal(reader(), 42);
    console.log("same reader after initialization:", reader());
}
console.log("TDZ, shadowing and timing checks passed");

第一行是缺失名称的 typeof 返回 undefined,随后两条断言确认块内读取 mode 和 typeof mode 都会报错。到 let mode 完成后,打印结果是 inner;离开块又恢复读取 outer。两个同名绑定彼此独立,外层值并没有被局部声明改写。

JavaScript 暂时性死区:外层已有同名变量,typeof 为什么仍然报错

AI生成概念示意图:局部名字已经占据作用域,但要等初始化完成后才能取出其中的值。

typeof 只对无法解析的名字提供特殊结果

typeof 遇到完全无法解析的标识符,可以返回字符串 undefined;遇到已经找到、但还没初始化的词法绑定,仍需要读取其值,于是产生异常。不能把“用 typeof 检查总是安全”当成通则,尤其是重构时刚在同一块里增加了同名声明的情况。

pending 的两次检查把另一条边界单独列出来:执行 let pending 之前仍会报错,声明语句执行之后,即使没有显式初值,也已经正常初始化为 undefined。判断状态时应问“初始化是否已经发生”,而不是只问源码里有没有写等号或当前值是不是空。

同名初始化不会自动向外层借值

示例外层 seed 是七,但块内的 let seed = seed 仍抛错。右边的名称已经解析到正在初始化的局部绑定,不能绕过它去读外层。若目的是从外层值派生新值,应换一个清楚的局部名字;末尾使用 localSeed 得到八,并断言外层七保持不变。

不要为了压住错误就机械地把 let 改成 var。两者的作用域与初始化规则不同,改写之后可能读到 undefined,也可能把原本局部的变量带到更大范围。应该先画出最内层块的边界,找到同名声明,再决定是调整名字还是安排初始化先后。

要看读取何时发生,而不是函数写在哪里

reader 在 ready 声明之前创建并不报错,因为创建箭头函数尚未执行函数体。第一次立刻调用 reader 时,ready 还没有初始化,断言捕获异常;完成声明以后调用同一个函数,就能返回四十二。这个对照把函数创建和变量读取两个时刻分开。

所以把代码搬进回调并不会自动解决问题,真正决定结果的是调用方什么时候执行它。同步调用、事件回调和模块初始化可能有不同顺序。这里没有使用定时器,也没有依赖等待时间,便于把问题直接定位到绑定初始化,随后再接回实际调用路径。

修复后至少保留三类检查:声明前读取确实失败,声明后得到预期值,离开局部块后外层值仍正确。对照错误类型即可,不必匹配某个引擎的整段报错文字。这样后续移动声明、抽取函数或增加同名配置时,测试能直接指出哪个读取越过了初始化边界。

资料核对日期:2026年10月2日(北京时间)。示例在 Node.js v24.19.0 中独立运行。

官方参考:ECMAScript 绑定读取规则;ECMAScript typeof 求值规则;MDN let 与暂时性死区。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。