梦亦同趋

【学习笔记】—暂时性死区

#暂时性死区

谈起暂时性死区(TDZ)就不得不提 JavaScript 的变量提升特性。

在 JS 中,varletconst 声明的变量都存在变量提升,区别在于:var 提升后会被初始化为 undefined 可以访问,而 letconst 提升后不会初始化,强行访问会报错。

乍一看有些奇怪——既然访问会报错,letconst 似乎没有提升的必要,毕竟"报错"和"访问一个不存在的变量"效果好像一样。但实际上这两种报错并不相同:

  • 暂时性死区:Uncaught ReferenceError: Cannot access '*' before initialization
  • 访问未声明的变量:Uncaught ReferenceError: * is not defined

为什么要区分这两种错误?看下面这个例子:

js

js
var tmp = 123; if (true) { console.log(tmp); // 报错: Cannot access 'tmp' before initialization let tmp; }

如果把 let 换成 varconsole.log 会打印 123,因为 var 没有块级作用域,if 块内外的 tmp 是同一个变量。但 let 具有块级作用域,if 块内是一个独立的作用域,块内的 let tmp 和外部的 var tmp 是两个不同的变量。

此时如果 let 不提升,console.log 就会"穿透"到外层作用域,读到外部的 123——但这会造成困惑,因为开发者在这个块里声明了自己的 tmp,却在声明前读到了一个"别人的" tmp。提升的意义正在于此:让引擎在进入块级作用域时就感知到 tmp 属于这里,从而阻止对外部同名变量的访问。

那为什么不直接报"变量未定义",而要专门区分一种新的报错类型?因为从整体上下文来看,tmp 确实是存在的(外部有 var tmp),报"未定义"会产生误导。TDZ 的报错更精确地告诉你:变量存在,但你在它初始化之前就访问了它

最后看这种情况:

js

js
var tmp = 123; if (true) { var tmp; console.log(tmp); // 123 }

打印的是 123 而不是 undefined。原因是 var 没有块级作用域,if 块内外的 tmp 是同一个变量,变量提升只会发生一次,重复的无赋值声明会被忽略,已有的值自然得以保留。