let
被提升到顶部的目的是什么,当访问时会抛出错误?
这样我们就可以拥有块级作用域,这是一个相当易于理解的概念,而不必像传统的 var
提升一样,产生错误和误解。
考虑这个块的内部:
{
let a = 1;
console.log(a);
let b = 2;
console.log(a, b);
let c = 3;
console.log(a, b, c);
}
设计者在这里有三个主要选择:
- 具有块级作用域,但所有声明都被提升到顶部并可访问(就像函数中的
var
一样);或
- 没有块级作用域,而是每个
let
、const
、class
等都有一个新的作用域开始;或
- 具有块级作用域,带有提升(或我所说的“半提升”),其中声明被提升,但它们声明的标识符在代码中不可访问,直到它们被调用。
选项1使我们容易出现与var
提升相同类型的错误。选项2对人们来说更加复杂,并且对JavaScript引擎的工作量更大(如果您想了解详情,请参阅下面的细节)。选项3达到了最佳效果:块级作用域易于理解和实现,并且TDZ可以防止由var
提升引起的错误。
另外,var
是否也会遭受TDZ的影响?我知道它何时会抛出undefined
,但这是因为TDZ吗?
不,var
声明没有暂时性死区。 undefined
不是“抛出”,它只是一个变量在声明但未被设置为其他值(尚)的值。var
声明提升到函数或全局环境的顶部,并在该范围内完全可访问,即使在到达 var
行之前。
了解JavaScript中如何处理标识符解析可能会有所帮助:
规范将其定义为词法环境,其中包含一个环境记录, 该记录包含有关变量、常量、函数参数(如果相关)、class
声明等当前上下文的信息。(上下文是作用域的特定执行。也就是说,如果我们有一个名为example
的函数,则example
的主体定义了一个新的作用域;每次调用example
时,该作用域都有新的变量等等,这就是上下文。)
有关标识符(变量等)的信息称为绑定。它包含标识符的名称、当前值以及有关它的一些其他信息(例如它是否可变、不可变、是否可以访问[但还没有访问]等)。
当代码执行进入新的上下文(例如,调用函数或进入包含let
或类似内容的块时),JavaScript引擎会创建一个新的词法环境对象(LEO),其中包含其环境记录(envrec),并将LEO链接到包含它的“外部”LEO,形成链。当引擎需要查找标识符时,它会在最顶层LEO的envrec中查找绑定,如果找到,则使用它;如果未找到,则查看链中的下一个LEO,直到达到链的末尾。(您可能已经猜到:链中的最后一个链接是全局环境。)
ES2015中为启用块作用域和let
、const
等所做的更改基本上是:
- 如果该块包含块作用域声明,则可以为块创建新的LEO
- LEO中的绑定可以被标记为“不可访问”,以便强制执行TDZ
有了这些,让我们来看看这段代码:
function example() {
console.log("alpha");
var a = 1;
let b = 2;
if (Math.random() < 0.5) {
console.log("beta");
let c = 3;
var d = 4;
console.log("gamma");
let e = 5;
console.log(a, b, c, d, e);
}
}
当调用example
时,引擎如何处理(至少是按照规范)?以下是:
- 它为调用
example
的上下文创建一个LEO。
- 它将
a
、b
和d
绑定添加到该LEO的envrec中,所有绑定的值都为undefined
:
- 加入
a
是因为它是函数中任何位置的var
绑定。由于是var
,因此设置其“可访问”标志为true。
b
被添加是因为它是函数顶层的let
绑定;由于我们还没有到达let b
行,因此将其“可访问”标志设置为false。
d
被添加是因为它像a
一样是var
绑定。
- 它执行
console.log("alpha")
。
- 它执行
a = 1
,将 a
的绑定值从 undefined
更改为 1
。
- 它执行
let b
,将 b
绑定的 "accessible" 标志设置为 true。
- 它执行
b = 2
,将 b
的绑定值从 undefined
更改为 2
。
- 它评估
Math.random() < 0.5
;假设它是真的:
- 因为该块包含块作用域标识符,引擎为该块创建一个新的 LEO,并将其 "outer" LEO 设置为 Step 1 中创建的那个。
- 它向该 LEO 的 envrec 添加
c
和 e
的绑定,将它们的 "accessible" 标志设置为 false。
- 它执行
console.log("beta")
。
- 它执行
let c = 3
,将 c
的绑定的 "accessible" 标志设置为 true,并将其值设置为 3
- 它执行
d = 4
。
- 它执行
console.log("gamma")
。
- 它执行
let e = 5
,将 e
的绑定的 "accessible" 标志设置为 true,并将其值设置为 5
。
- 它执行
console.log(a, b, c, d, e)
。
希望这能解答以下问题:
- 为什么我们有
let
半提升(使其易于理解范围,并避免具有太多 LEO 和 envrecs 的块级别错误,以及避免像函数级别的 var
提升一样的错误)
- 为什么
var
没有 TDZ(var
变量绑定的“可访问”标志始终为真)
* 至少在规范方面是这样的。实际上,只要它的行为符合规范定义,它们可以做任何它们喜欢的事情。事实上,大多数引擎会做更有效率的事情,利用堆栈等。