let、const 在声明前访问为什么抛 ReferenceError?时态死区 TDZ 如何理解
let、const 在声明前访问为什么抛 ReferenceError时态死区 TDZ 如何理解【免费下载链接】33-js-concepts 33 JavaScript concepts every developer should know.项目地址: https://gitcode.com/GitHub_Trending/33/33-js-concepts写 JavaScript 时下面这段代码会直接抛错console.log(name) // ReferenceError: Cannot access name before initialization let name Alice而把let换成var就只打印undefined不报错。这个差异来自时态死区Temporal Dead ZoneTDZ。本文基于 33-js-concepts 仓库的 TDZ 文档 和配套的 vitest 测试文件 temporal-dead-zone.test.js说明 TDZ 的边界、它在哪些语法里会出现以及如何在这个仓库里实际跑起来验证每一种行为。文档声明了前置要求需要先理解作用域与闭包全局、函数、块级作用域的区别可先复习 scope-and-closures。TDZ 是进入作用域到执行声明之间的区间按 TDZ 文档 的定义TDZ 是从进入一个作用域开始、到let/const/class声明那一行被初始化为止的区间。在这个区间内变量已经存在引擎知道它但任何访问都会抛ReferenceError。文档引用 ECMAScript 规范的说法是let和const的绑定在其所在环境记录实例化时就被创建但在声明被求值之前保持未初始化状态。TDZ 的起止位置在一个块里可以这样标注{ // TDZ for x starts here (beginning of block) console.log(x) // ReferenceError: Cannot access x before initialization let x 10 // TDZ for x ends here console.log(x) // 10 (works fine) }会产生 TDZ 的声明包括let声明const声明class声明函数默认参数特定情况下类的静态字段为什么 var 不抛错而是打印 undefinedvar和let/const一样会被提升hoist区别在初始化这一步方面varlet/const会被提升吗是是提升时初始化吗是初始化为undefined否保持未初始化声明前访问的结果返回undefined抛ReferenceError有 TDZ 吗没有有文档给出的对照代码function varExample() { console.log(x) // undefined (not an error!) var x 10 console.log(x) // 10 } function letExample() { console.log(y) // ReferenceError: Cannot access y before initialization let y 10 console.log(y) // never reaches here }Hoisting 文档 特别指出一个常见误解很多教程说let/const没有被提升这是不正确的——它们确实被提升只是在 TDZ 中保持未初始化直到声明执行。Temporal 指的是执行时间不是代码位置TDZ 之所以叫时态死区是因为它取决于代码何时执行而不是变量在源码中的位置。文档的例子{ // TDZ for x starts here const getX () x // 定义函数此时并未访问 x let x 42 // TDZ 结束 console.log(getX()) // 42 - works! }getX在 TDZ 期间被定义没有问题它在x初始化之后才被调用所以能正常返回 42。反过来如果在 TDZ 期间就调用它{ const getX () x // OK: just defining, not accessing getX() // ReferenceError! Calling during TDZ let x 42 getX() // 42 - now it works }也就是说TDZ 只在真正访问变量时才起作用定义一个稍后会访问它的函数是完全安全的。在仓库里运行 TDZ 测试文件来验证上面的示例如果直接交给node执行进程会因ReferenceError中断所以仓库用 vitest 把它们包进断言里验证。temporal-dead-zone.test.js 覆盖了基本行为、TDZ 边界、typeof、默认参数、解构、循环、class、静态字段、遮蔽等场景断言方式形如expect(() { eval( const value x let x 10 ) }).toThrow(ReferenceError)验证步骤在仓库根目录执行npm install安装 package.json 声明的 devDependencies包括vitest ^4.0.16和jsdom依赖会写入根目录的node_modules。只运行 TDZ 这一个测试文件测试匹配规则见 vitest.config.js为tests/**/*.test.jsnpx vitest run tests/beyond/language-mechanics/temporal-dead-zone/temporal-dead-zone.test.js成功条件是全部用例通过其中应包含let/const声明前访问抛ReferenceError、声明后访问得到初始值、var声明前访问返回undefined、typeof在 TDZ 中抛错、循环中let每次迭代产生新绑定等。注意 package.json 里的npm test即vitest run会运行tests/下全部概念测试不只是 TDZ只想核对 TDZ 行为时使用上面的单文件命令即可。typeof 在 TDZ 中不是安全网typeof对未声明的变量是安全的返回undefinedconsole.log(typeof undeclaredVar) // undefined (no error)但对处于 TDZ的变量会抛ReferenceError因为引擎知道该变量存在已被提升所以强制执行 TDZ 限制{ console.log(typeof x) // ReferenceError: Cannot access x before initialization let x 10 }这经常让依赖typeof做安全检测的代码意外崩溃。文档给出的规则不要单独依赖typeof判断变量是否存在而是把变量声明放在需要检测之前。默认参数与解构从左到右的 TDZ 规则函数默认参数和对象解构都按从左到右求值后面的可以引用前面的反过来就是 TDZ 错误// Works: b can reference a function example(a 1, b a 1) { return a b // 1 2 3 } // Fails: a cannot reference b (TDZ!) function broken(a b, b 2) { return a b // ReferenceError }解构遵循同样规则自引用也是 TDZ 错误// Works: b can use as default let { a 1, b a 1 } {} console.log(a, b) // 1, 2 // Fails: a cannot use b (TDZ!) let { a b, b 1 } {} // ReferenceError // 自引用 let { x x } {} // ReferenceError: Cannot access x before initialization自引用报错的原因右边的x指向正在声明的那个x求值时它还在 TDZ 中。循环头部自引用与每次迭代的新绑定for...of/for...in的循环变量在头部求值时处于 TDZ所以拿它引用自身会抛错测试文件里两种写法都有覆盖// This throws because n is used in its own declaration for (let n of n.values) { // ReferenceError console.log(n) }let在循环中还有一个与 TDZ 相关的关键行为每次迭代获得一个全新的绑定const funcs [] for (let i 0; i 3; i) { funcs.push(() i) } console.log(funcs[0]()) // 0 console.log(funcs[1]()) // 1 console.log(funcs[2]()) // 2而var的闭包共享同一个变量三个调用都返回3。这正是let在循环中能避开经典闭包陷阱的原因。最容易踩的 TDZ 陷阱遮蔽外层变量文档把变量遮蔽shadowing列为最常见的 TDZ 陷阱// 错误示例 const x 10 function example() { console.log(x) // ReferenceError! Inner x is in TDZ let x 20 // 遮蔽了外层的 x return x } example() // ReferenceError!内部的let x遮蔽了外部的const x。在内部声明之前读x时JavaScript 看到的是内部那个x此时在 TDZ 中而不是外层那个所以抛错。修复方式按文档给出的三条内外值都要用时改用不同的变量名在声明内部变量之前先把外层值捕获下来让声明排在使用之前。// 正确示例 const x 10 function fixed() { const outerX x // Capture outer x first let y 20 // Use different name, no shadowing return outerX y // Use both: 10 20 30 }ES 模块循环引用导入的值还在 TDZ 中模块 A 和 B 互相导入时一方执行到访问对方导出的值时对方可能还没执行到那行声明于是触发 TDZ 的ReferenceError。文档的例子// -- a.js (entry point) -- import { b } from ./b.js console.log(a.js: b , b) // 1 export const a 2// -- b.js -- import { a } from ./a.js console.log(b.js: a , a) // ReferenceError! export const b 1执行流程是a.js启动 → 遇到import { b }暂停自己去加载b.js→b.js里的import { a }创建了绑定但a.js还没执行到export const a→b.js访问a时它仍在 TDZ → 抛错。文档给出三种解法方案 1延迟访问—— 不在顶层立即使用导入值放进稍后运行的函数里// -- b.js (fixed) -- import { a } from ./a.js export const b 1 // Access a later, when its definitely initialized export function getA() { return a }方案 2重组模块—— 把共享代码抽到一个双方都依赖的模块打破循环// -- shared.js -- export const a 2 export const b 1 // -- a.js / b.js 都改为从 ./shared.js 导入方案 3动态导入—— 用import()推迟加载// -- b.js -- export const b 1 export async function getA() { const { a } await import(./a.js) return a }文档还给了一个排查提示如果对某个你确定存在的导入值看到ReferenceError先检查是否存在循环导入报错信息Cannot access X before initialization是模块中 TDZ 的典型信号。判断与规避要点把上述行为收拢成可核对的判断TDZ 是进入作用域到声明被初始化之间的区间期间访问let/const/class抛ReferenceErrorvar没有 TDZ声明前访问得到undefined。temporal 指执行时间TDZ 期间定义引用该变量的函数没问题调用它才有问题。typeof在 TDZ 中同样抛错不能当作安全检测手段。默认参数与解构按从左到右求值右侧引用前面的可以、引用后面的抛错。静态字段引用尚未定义的后续字段返回的是undefined属性访问不抛ReferenceError但在类声明之前访问类名本身仍会抛错const x MyClass.value在class MyClass之前会抛ReferenceError。文档给出的最简单规避方式是声明排在使用之前对遮蔽场景先捕获外层值或改用不同变量名。核对完理解后可以随时用npx vitest run tests/beyond/language-mechanics/temporal-dead-zone/temporal-dead-zone.test.js重跑测试文件如果之后在项目中遇到Cannot access X before initialization且确定该值存在优先按上一节的循环导入排查路径检查模块间的相互导入。【免费下载链接】33-js-concepts 33 JavaScript concepts every developer should know.项目地址: https://gitcode.com/GitHub_Trending/33/33-js-concepts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考