ES6中let变量提升的目的是什么?

6
我明白 let 会被提升到块的顶部,但在初始化之前访问它将抛出 ReferenceError,因为它处于 Temporal Dead Zone
例如:
console.log(x);   // Will throw Reference Error
let x = 'some value';

但是像这样的片段将不会出现错误:
foo(); // alerts foo;
function foo(){    // foo will be hoisted 
  alert("foo");
} 

我的问题

let变量提升到顶部的目的是什么,如果访问它会抛出错误?另外,var也遭受TDZ的影响吗?我知道它会抛出undefined,但这是因为TDZ吗?


可能是"let"关键字和"var"关键字的重复问题。 - Marcos Pérez Gude
1
@MarcosPérezGu 我的疑问与提升相关。即使访问时出错,let 的提升是否有任何不同之处?但是你分享的链接告诉我们更多关于 let 和 var 之间的区别。 - brk
好的,抱歉,我不知道什么是提升,但现在一切都清楚了。我撤销了我的重复投票。 - Marcos Pérez Gude
可能是变量提升有什么用处?的重复问题。 - Bergi
5个回答

4

文档中提到:

变量在其所在的词法环境实例化时创建,但在词法绑定被求值之前不能以任何方式访问。使用初始化器定义的词法绑定中定义的变量在词法绑定求值时被分配为其初始化器的赋值表达式的值,而不是在变量创建时分配。如果let声明中的词法绑定没有初始化器,则在词法绑定求值时将变量分配为未定义的值。

此外,var关键字

let 允许您声明范围仅限于块、语句或表达式的变量。 这与 var 关键字不同,后者无论块作用域如何,都会在全局范围内或整个函数的本地范围内定义变量。

您还可以查看Kyle Simpson的这篇文章:支持和反对let


1

http://www.2ality.com/2015/10/why-tdz.html 很好地解释了这个问题,并且还链接到了https://mail.mozilla.org/pipermail/es-discuss/2012-September/024996.html,这是一个相关的讨论。

对于这个问题的内容进行改述:

为什么 let 存在暂时性死区?

  1. 如果 TDZ 没有引起引用错误,并且您在变量声明之前访问了该变量(即在 TDZ 中),那么您很可能会遗漏一个编程错误。TDZ 导致的引用错误帮助您捕捉编程错误。

  2. 那么您接下来的问题是 - 为什么甚至要为 let 设计 TDZ? 为什么不在其声明时开始 let 变量的作用域呢?答案是 const。TDZ 是为 const 设计的,而 (可怜的) let 只是为了更容易地在 letconst 之间切换而被卡在了 TDZ 中。


var也会遭受TDZ的影响吗?我知道它会在何时抛出undefined,但那是因为TDZ的原因吗?

不会,var不会受到TDZ的影响。它不会抛出任何错误,只是在设置之前保持undefined。TDZ是ES6的一个特性。


0

你必须先理解变量提升。这意味着将代码声明的初始化移到块的顶部,考虑以下示例:

function getValue(condition) {
    if (condition) {
        var value = "blue";
        // other code
        return value;
    } else {
        // value exists here with a value of undefined
        return null;
    }
        // value exists here with a value of undefined
}

正如您所看到的,该值可以在else语句和函数中访问。因为它是在getValue(condition)函数之后直接声明的。

function getValue(condition) {
    if (condition) {
        let value = "blue";
        // other code
        return value;
    } else {
        // value doesn't exist here
        return null;
    }
    // value doesn't exist here
}

但是当我们使用let时,你可以看到区别。这些例子来自我正在阅读的一本书,我建议你也去看看。

https://leanpub.com/understandinges6/read#leanpub-auto-var-declarations-and-hoisting

为了进一步澄清


0

let 被提升到顶部的目的是什么,当访问时会抛出错误?

这样我们就可以拥有块级作用域,这是一个相当易于理解的概念,而不必像传统的 var 提升一样,产生错误和误解。

考虑这个块的内部:

{
    let a = 1;
    console.log(a);
    let b = 2;
    console.log(a, b);
    let c = 3;
    console.log(a, b, c);
}

设计者在这里有三个主要选择:

  1. 具有块级作用域,但所有声明都被提升到顶部并可访问(就像函数中的var一样);或
  2. 没有块级作用域,而是每个letconstclass等都有一个新的作用域开始;或
  3. 具有块级作用域,带有提升(或我所说的“半提升”),其中声明被提升,但它们声明的标识符在代码中不可访问,直到它们被调用。

选项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中为启用块作用域和letconst等所做的更改基本上是:

  • 如果该块包含块作用域声明,则可以为块创建新的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时,引擎如何处理(至少是按照规范)?以下是:

  1. 它为调用example的上下文创建一个LEO。
  2. 它将abd绑定添加到该LEO的envrec中,所有绑定的值都为undefined
  • 加入a是因为它是函数中任何位置的var绑定。由于是var,因此设置其“可访问”标志为true。
  • b被添加是因为它是函数顶层的let绑定;由于我们还没有到达let b行,因此将其“可访问”标志设置为false。
  • d被添加是因为它像a一样是var绑定。
  1. 它执行 console.log("alpha")
  2. 它执行 a = 1,将 a 的绑定值从 undefined 更改为 1
  3. 它执行 let b,将 b 绑定的 "accessible" 标志设置为 true。
  4. 它执行 b = 2,将 b 的绑定值从 undefined 更改为 2
  5. 它评估 Math.random() < 0.5;假设它是真的:
  6. 因为该块包含块作用域标识符,引擎为该块创建一个新的 LEO,并将其 "outer" LEO 设置为 Step 1 中创建的那个。
  7. 它向该 LEO 的 envrec 添加 ce 的绑定,将它们的 "accessible" 标志设置为 false。
  8. 它执行 console.log("beta")
  9. 它执行 let c = 3,将 c 的绑定的 "accessible" 标志设置为 true,并将其值设置为 3
  10. 它执行 d = 4
  11. 它执行 console.log("gamma")
  12. 它执行 let e = 5,将 e 的绑定的 "accessible" 标志设置为 true,并将其值设置为 5
  13. 它执行 console.log(a, b, c, d, e)
希望这能解答以下问题:
  • 为什么我们有 let 半提升(使其易于理解范围,并避免具有太多 LEO 和 envrecs 的块级别错误,以及避免像函数级别的 var 提升一样的错误)
  • 为什么 var 没有 TDZ(var 变量绑定的“可访问”标志始终为真)

* 至少在规范方面是这样的。实际上,只要它的行为符合规范定义,它们可以做任何它们喜欢的事情。事实上,大多数引擎会做更有效率的事情,利用堆栈等。


1
有点晚了,但是为什么你说在提升时 ce 都会将它们的可访问标志设置为 false,并将它们的值设置为 undefined?难道不应该只有前者吗?如果在实际定义之前尝试访问它们,难道不应该抛出异常而不是返回 undefined 吗? - 4javier
1
@4javier - 发现得非常好,谢谢!没错,CreateImmutableBinding只是创建绑定并记住它未初始化,它不会将绑定的值设置为undefined(为什么要这样做?毕竟没有任何东西可以访问该值)。稍后显示绑定初始化(let c)与设置其值(c = 3)之间的分离的部分也是不正确的,它们是同时完成的... - T.J. Crowder
1
感谢您指出这一点!我已经修复了上面的两个问题。在InitializeBinding中实现。再次感谢! - T.J. Crowder

-1

一个let变量不会被提升。let变量称为“被提升”在技术上是正确的,但我认为这种用法会误导人。描述语义的等价方法是,在其声明以上尝试引用它时会得到ReferenceError,因为它还不存在;如果你尝试引用该块中不存在的变量,你将得到相同的结果。

更多信息:

C++和JavaScript都有块作用域,但在此特定点上有所区别,因此我们可以通过理解它们的差异来理解这一点。考虑以下示例:

#include <iostream>                                                         

int main() {
    int x = 3;

    {
        std::cout << x << std::endl;
        int x = 4;
    }

    return 0;
}

在C++中,实际上没有提升,当cout行运行(打印x到屏幕)时,第二个x不存在,但第一个x仍然存在,因此程序顺从地打印3。这非常令人困惑。相反,我们应该考虑将对x的引用视为模棱两可并将其视为错误。
以下是类似的JavaScript代码发生的情况:
'use strict';                                                               

let x = 3;

(() => {
    console.log(x);
    let x = 4;
})();

在JavaScript中,通过“提升”第二个x来解决了这个问题,但是在访问时会抛出ReferenceError。据我所知,这种“提升”等同于由于歧义而使对x的引用成为错误。

let会将变量提升到块的顶部。https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/let - brk
好的,规范将其描述为提升,但在我看来,这是一个误导性的描述。当你实现规范时,得到的行为与没有提升且变量超出作用域时的行为相同。 - voltrevo
@user2181397,我添加了一个编辑,更详细地解释了。 - voltrevo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接