为什么存在临时死区?

6
我知道什么是暂时性死区(TDZ),但我不明白它存在的目的。
有人能解释一下为什么它被创建了吗?
它引发ReferenceErrorSyntaxError而不是返回undefined的逻辑是什么? ReferenceError:

console.log(typeof foo);
let foo;

语法错误:

let foo;
let foo;

未定义:

console.log(typeof foo);


未定义可能有多种原因。是引用错误还是语法错误可以更好地告诉您出了什么问题。 - Shilly
@Shilly,如果那是原因,那么引起未定义的多种情况会导致ReferenceError或SyntaxError,而不仅仅是因为它提供了更多信息。这其中必须还有其他原因。 - user7393973
这不是一个很大的假设吗?人们已经在“let”规范上工作了多年。Vanilla JS是在10天内编写的?让它返回未定义对我来说没有太多意义。当前的“let”处理方式更符合其他编程语言,因此让JavaScript不再成为具有特殊变量规则的杰出代表可能是有价值的。我会包含一些链接,也许其中一个有更多的论据。https://rainsoft.io/variables-lifecycle-and-why-let-is-not-hoisted/ 和 https://davidwalsh.name/for-and-against-let - Shilly
2
为什么ES6中会有“暂时性死区”? - Andreas
你还应该了解一下“变量提升”。这是JS中的一个重要概念,以前在面试问题中常被用来考验你。使用“let”可以消除隐含的不可预测性(如果你习惯了它,那么就没问题,否则可能会有问题)。 - Tim Consolazio
@Andreas,那几乎是唯一谈论这个主题的地方,但它有点模糊,难以理解,并且在我看来缺少了一些信息。 - user7393973
4个回答

3

在初始化变量之前使用它是一个错误,这种错误应该被视为一种错误类型,因为它可以帮助开发人员注意到他们的错误并进行修正。变量的行为从undefined开始已经引起了太多的问题,如果类似的语义用于const变量或静态类型注释,问题将会更加严重。


3
他们肯定也希望将 "var" 也归为错误,但是 JavaScript 必须保持向后兼容以避免破坏网络。你还记得 ES5 的严格模式吗?它会在未声明的变量上引发引用错误。但是在松散模式下仍然不会发生。 - Bergi
1
那不可能是这样的,因为如果问题是这样的话,他们通常会将其弃用并在将来删除,就像他们通常做的那样,@Bergi。就像for each...in一样。 - user7393973
@user7393973 不,他们不能删除它。废弃它(类似于__proto__)可能是可能的,但不值得; 而且仍有许多合理的使用场景,例如在全局范围内使用 var(TDZ不是它们唯一的区别)。关于 for each,它从未成为标准(由于ES4失败了),而只是 Mozilla 的一个实验性扩展。 - Bergi
1
是的,基本上所有这些都是ES3错误、Mozilla特定的语法特性(已经得到改进,现在在ES6中有所不同),或者被证明鼓励了不良实践的东西。var不属于这些。 - Bergi
我正在这里绞尽脑汁想一个你有意在变量初始化之前使用它的理由,唯一我能想到的那些合理的解释也可以通过预定义布尔值来解决,并且使用 ! 比使用 typeof 检查更省击键次数。因此,唯一的理由就是如果你需要练习打字,并且在输入完成后将删除代码。 - Chris Baker
显示剩余4条评论

2

变量一旦被定义就应该存在,这是有道理的。由于来自外部作用域的变量在嵌套作用域中可以访问,因此以下代码非常令人困惑:

var foo = 'out';

function bar () {
  foo = 'in';
  console.log(foo);
  var foo;
}

bar();

函数 bar() 是干什么的?它创建了一个名为 foo 的新变量,并将 'in' 赋值给它,然后在控制台中显示它。之后,来自外部作用域的变量仍然等于'out'。因此,在使用变量之前定义它们是更明智的选择。我相信,你可以在声明变量之前使用变量只是出于实现简单和效率的考虑,但这只是我的大胆猜测。

然而,在 JavaScript 中,使用 var 关键字创建的变量只能从它们的函数中访问,而不能从它们的块中访问。这允许使用更宽松的语法,如下例:

function setGameMode (mode) {
  if (mode === 'peaceful') {
    var count = 0;
    for (var i = 0; i < mobs.length; ++i) {
      if (mobs[i] instanceOf EvilMob) {
        mobs[i].despawn();
        ++count;
      }
    }
    console.log('Removed ' + count+ ' evil mobs out of ' + i);
    mobSpawner.evil = false;

  } else if (mode ==='chaotic') {
    var count = 0;
    for (var i = 0; i < mobs.length; ++i) {
      if (mobs[i] instanceOf NiceMob) {
          mobs[i].despawn();
          ++count;
      }
    }
    console.log('Removed ' + count + ' nice mobs out of ' + i);
    mobSpawner.nice = false;
  }
}
i变量在for循环后仍然存在,这要归功于函数作用域变量。这也是为什么var关键字允许您定义变量两次的原因。如果您被迫只写一次var,那将不太实用。对于let变量,这种“宽松”的特性变得无用,因为这些变量应该尽快释放。
在大多数编程语言中调用函数时,执行环境会为您需要的变量创建一个内存空间。在这里,由于变量i在if块内部,无法确定是否需要使用它,除非实际运行代码。但是,由于JS具有函数作用域变量,它必须从函数开始就创建变量。对于let变量来说,这样做是没有必要的。
至于typeof someundefinedvar返回“undefined”的原因,那是因为您需要一种检查可能已在外部范围中声明的变量的方法。但是,对于块级作用域的变量,您不需要该功能。let变量应该立即使用并丢弃。

我还在阅读你的回答,但函数内的 var foo; 导致了奇怪的问题。但我不认为它应该表现出这种方式,看起来像是一个 bug。 - user7393973
是的,但这就是JS的工作方式。var foo在函数内创建了局部变量foo,它遮蔽了外部的foo。最好避免使用这样的代码,它很糟糕^^ - Domino
好的解释,但话题有点混乱。现在我对任何事情都不确定了。在第一段代码中,我不明白为什么会发生这种情况。我的经验告诉我:var foo = 'out';创建了一个全局变量(至少对于其他代码来说是这样)。然后在函数内部,foo = 'in';改变了它的值。var foo;应该只是声明了一个本地(函数作用域)变量。 - user7393973
1
不错的示例代码,挑剔一点:在循环结束时,i变量应该等于所有mobs的数量-1。如果只有好或坏的mobs被清除,可能是总数的非100%,无论发生什么事情,告诉日志所有mobs-1都被清除了并不好。 :) 示例仍然传达了您要解释的概念,我知道这是一个吹毛求疵的观点。 - Chris Baker
@ChrisBaker 你说得对,实际上代码并没有达到输出所暗示的效果。这个修复应该解决了它 :) - Domino
显示剩余5条评论

0
  1. console.log(typeof a); let a;

在上述语句中,您没有定义变量a而是使用它。在JavaScript中,定义变量是必要的。该变量是您值的引用,因此会显示未定义的引用错误。

  1. let a; let a;

在第二个语句中,您定义了一个变量两次。js不允许您在同一块作用域中定义任何变量超过一次。

  1. console.log(typeof foo);

    在第3个语句中,由于第一个函数显示变量未定义的错误。默认情况下,它未定义。您可以使用未定义的变量。


2
你忽略了问题,即“为什么let和var不同”。情况1和2可以使用var,但不能使用let。你没有解释为什么情况3与情况1不同。 - Domino
就是 @JacqueGoupil 刚才说的。 - user7393973
var 和 let 的区别在于“作用域”。let 有限的作用域,而 var 是全局的。你可以在任何地方使用它。 - Sudhanshu Jain
不,他在问为什么它们会抛出不同的错误。我认为问题的关键是向后兼容性。很可能,在未来的EMCA脚本版本中,会有一个BC断点并开始抛出一致的错误,因为使用未定义的变量在编程上是相同的错误,无论是let还是var。 - Chris Baker

0

如果您查看您在问题中提供的那些异常的文档,您可以看到其中的原因,我认为这里没有讨论。

首先要注意的是,直到第3版才出现EMCAScript中的异常。var显然比这个语言特性早很多。为什么var和let不同主要是由于在引入let时已经可用异常,而在引入var时不可用异常。这段历史强调了所有这些内容。1999年也很忙碌!

无论如何,进入齿轮。看看这里发生了什么:

// example A
(function () {
console.log(somethingUndefined - 1);
var somethingUndefined;
console.log('another operation');
})();

出现错误,然后代码继续执行。我们能捕捉到它并处理吗?

// example B
(function () {
try {
    console.log(somethingUndefined - 1);
    var somethingUndefined = 50;
    console.log('another operation');
} catch (e) {
    console.log('I want to deal with problems here');
    return;
}

console.log('plowing on');
})();

不行。

幕后,这段代码看起来像这样:

// example C
(function () {
var somethingUndefined = undefined;
try {
    console.log(somethingUndefined - 1);
    somethingUndefined = 50;
    console.log('another operation');
} catch (e) {
    console.log('I want to deal with problems here');
    return;
}

console.log('plowing on');
})();

不存在“暂时性死区”,因为在示例B和C中,变量 somethingUndefined 从未不是某个值。它是一种typeof“undefined”, somethingUndefined === undefined ,但这并不意味着没有任何值。然后它的值为50,但太晚了,已经没用了。无论有用与否,我们都可以使用它来完成任务,因为它有一个值。在第1行和第8行,它始终有一些值。在此处比较输出上的差异:

// example D
(function () {
try {
    console.log(somethingUndeclared - 1);
    console.log('another operation');
} catch (e) {
    console.log('I want to deal with problems here');
    console.log(e);
}
})();

在 D 示例中,somethingUndeclared 在任何时候都是不存在的。它始终处于死区状态。我不确定以上代码从哪个 EMCAScript 版本开始引发异常,但它这样做会更好,因为这种情况永远是错误的。
除非在同一作用域或父级作用域的其他部分定义了 somethingUndeclared,可能是为了其他目的。或者代码 var somethingUndeclared; 在作用域或父级作用域中某个地方闲置着,所以在上述代码中没有抛出任何异常。这就是几乎完全使用 let 或 const 的理由。
当您使用 let 或 const 时,存在一个“死区”,即它在某个时间点并不是任何东西,然后变成了某个东西。它不是undefined,它是一个等待发生异常的例外。在到达 let 语句之前,它的行为类似于未声明的变量,会抛出异常,然后在声明行上,它的行为类似于带有值的 var。它是 var 行为和未声明变量结果的混合体。

// example D
(function () { // temporal dead zone, something is nothing
try { // temporal dead zone, something is nothing
    console.log(something - 1); // exceptional behavior! temporal dead zone
    let something  = 50; // temporal live zone begins here!
    console.log('another operation');
} catch (e) {
    console.log('I want to deal with problems here'); // you could call this a dead zone
    console.log(e); // dead
}
// dead
})(); // way dead

这让我们想到了一个问题:“为什么不直接解决BC(向后兼容性)问题,为了语言的一致性?”答案是因为通常情况下“够好”就足够了。有数百万行的JavaScript代码来管理手风琴、图库和其他UI效果。当EMCAScript在填补关键任务时,人们可以选择更成熟的语言特性。或者你只是不喜欢你甜美的选项卡导航中出现杂乱无章的代码。语言的一致性并不值得打破所有已经存在的代码,这些代码大多数情况下都足以完成它们需要做的事情。想象一下自从这种语言('99)能够抛出异常以来积累的大量足够好的代码。
当我们需要它始终按照预期工作时,我们可以利用这些特性。当在项目中某个文件中出现undefined = 100;将会是一场灾难时,我们可以选择使用异常处理。当渐进增强失败并且我们没有工具提示时,我们的用户体验就不如我们希望的那样好。var和let有着不同的历史和不同的负担,所以它们很可能会一直表现不同。

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