Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS变量生命周期:为什么 let 没有被提升 #79

Open
husky-dot opened this issue Jul 18, 2019 · 0 comments
Open

JS变量生命周期:为什么 let 没有被提升 #79

husky-dot opened this issue Jul 18, 2019 · 0 comments

Comments

@husky-dot
Copy link
Owner

译者:前端小智

原文:https://dmitripavlutin.com/variables-lifecycle-and-why-let-is-not-hoisted/

为了保证的可读性,本文采用意译而非直译。

提升是将变量或函数定义移动到作用域头部的过程,通常是 var 声明的变量和函数声明function fun() {...}

当 ES6 引入let(以及与let类似声明的constclass)声明时,许多开发人员都使用提升定义来描述如何访问变量。但是在对这个问题进行了更多的探讨之后,令我惊讶的是提升并不是描述let变量的初始化和可用性的正确术语。

ES6 为let提供了一个不同的和改进的机制。它要求更严格的变量声明,在定义之前不能使用,从而提高代码质量。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

1. 容易出错的 var 提升

有时候我们会在zuo内作用域内看到一个奇怪的变量var varname和函数函数function funName() {...} 声明:

// var hoisting
num;     // => undefined
var num;
num = 10;
num;     // => 10
// function hoisting
getPi;   // => function getPi() {...}
getPi(); // => 3.14
function getPi() {
  return 3.14;
}

变量num在声明var num之前被访问,因此它被赋值为undefinedfucntion getPi(){…}在文件末尾定义。但是,可以在声明getPi()之前调用该函数,因为它被提升到作用域的顶部。

事实证明,先使用然后声明变量或函数的可能性会造成混淆。假设您滚动一个大文件,突然看到一个未声明的变量,它到底是如何出现在这里的,以及它在哪里定义的?

当然,一个熟练的JavaScript开发人员不会这样编写代码。但是在成千上万的JavaScript中,GitHub repos是很有可能处理这样的代码的。

即使查看上面给出的代码示例,也很难理解代码中的声明流。

当然,首先要声明再使用。let 鼓励咱们使用这种方法处理变量。

2. 理解背后原理:变量生命周期

当引擎处理变量时,它们的生命周期由以下阶段组成:

  1. **声明阶段(Declaration phase)**是在作用域中注册一个变量。

  2. **初始化阶段(Initialization phase)**是分配内存并为作用域中的变量创建绑定。 在此步骤中,变量将使用undefined自动初始化。

  3. **赋值阶段(Assignment phase)**是为初始化的变量赋值。

变量在通过声明阶段时尚未初始化状态,但未达到初始化状态。

请注意,就变量生命周期而言,声明阶段与变量声明是不同的概念。 简而言之,JS引擎在3个阶段处理变量声明:声明阶段,初始化阶段和赋值阶段。

3.var 变量的生命周期

熟悉生命周期阶段之后,让我们使用它们来描述JS引擎如何处理var变量。

假设JS遇到一个函数作用域,其中包含var变量语句。变量在执行任何语句之前通过声明阶段,并立即通过作用域开始处的初始化阶段(步骤1)。函数作用域中var变量语句的位置不影响声明和初始化阶段。

在声明和初始化之后,但在赋值阶段之前,变量具有undefined 的值,并且已经可以使用。

在赋值阶段variable = 'value' 时,变量接收它的初值(步骤2)。

严格意义的提升是指在函数作用域的开始处声明并初始化一个变量。声明阶段和初始化阶段之间没有差别。

让我们来研究一个例子。下面的代码创建了一个包含var语句的函数作用域

function multiplyByTen(number) {
  console.log(ten); // => undefined
  var ten;
  ten = 10;
  console.log(ten); // => 10
  return number * ten;
}
multiplyByTen(4); // => 40

开始执行multipleByTen(4)并进入函数作用域时,变量ten在第一个语句之前通过声明和初始化步骤。因此,当调用console.log(ten)时,打印undefined。语句ten = 10指定一个初值。赋值之后,console.log(ten) 将正确地打印10

4. 函数声明生命周期

在函数声明语句function funName() {...}的情况下,它比变量声明生命周期更简单。

声明、初始化和赋值阶段同时发生在封闭函数作用域的开头(只有一步)。可以在作用域的任何位置调用funName(),而不依赖于声明语句的位置(甚至可以在末尾调用)。

下面的代码示例演示了函数提升:

function sumArray(array) {
  return array.reduce(sum);
  function sum(a, b) {
    return a + b;
  }
}
sumArray([5, 10, 8]); // => 23

当执行sumArray([5,10,8])时,它进入sumArray函数作用域。在这个作用域内,在任何语句执行之前,sum都会通过所有三个阶段:声明、初始化和赋值。这样,array.reduce(sum)甚至可以在它的声明语句sum(a, b){…}之前使用sum

5. let 变量的生命周期

let 变量的处理方式与var不同,主要区别在于声明和初始化阶段是分开的

现在来看看一个场景,当解释器进入一个包含let变量语句的块作用域时。变量立即通过声明阶段,在作用域中注册其名称(步骤1)。

然后解释器继续逐行解析块语句。

如果在此阶段尝试访问变量,JS 将抛出 ReferenceError: variable is not defined。这是因为变量状态未初始化,变量位于暂时死区 temporal dead zone

当解释器执行到语句let variable时,传递初始化阶段(步骤2)。变量退出暂时死区。

接着,当赋值语句variable = 'value'出现时,将传递赋值阶段(步骤3)。

如果JS 遇到let variable = 'value',那么初始化和赋值将在一条语句中发生。

让我们看一个例子,在块作用域中用 let 声明变量 number

let condition = true;
if (condition) {
  // console.log(number); // => Throws ReferenceError
  let number;
  console.log(number); // => undefined
  number = 5;
  console.log(number); // => 5
}

当 JS 进入if (condition) {...} 块作用域,number立即通过声明阶段。

由于number已经处于单一化状态,并且处于的暂时死区,因此访问该变量将引发ReferenceError: number is not defined。接着,语句let number进行初始化。现在可以访问变量,但是它的值是undefined

constclass 类型与let具有相同的生命周期,只是分配只能发生一次。

5.1 提升在let生命周期中无效的原因

如上所述,提升是变量在作用域顶部的耦合声明和初始化阶段。然而,let生命周期分离声明和初始化阶段。解耦消除了let的提升期限。

这两个阶段之间的间隙产生了暂时死区,在这里变量不能被访问。

总结

使用var声明变量很容易出错。在此基础上,ES6 引入了let。它使用一种改进的算法来声明变量,并附加了块作用域。

由于声明和初始化阶段是解耦的,提升对于let变量(包括constclass)无效。在初始化之前,变量处于暂时死区,不能访问。

为了保持变量声明的流畅性,建议使用以下技巧

  • 声明、初始化然后使用变量,这个流程是正确的,易于遵循。

  • 尽量隐藏变量。公开的变量越少,代码就越模块化。

番外

如何理解 let x = x 报错之后,再次 let x 依然会报错?

这个问题说明:如果 let x 的初始化过程失败了,那么

  1. x 变量就将永远处于 created 状态。

  2. 你无法再次对 x 进行初始化(初始化只有一次机会,而那次机会你失败了)。

  3. 由于 x 无法被初始化,所以 x 永远处在暂时死区

  4. 有人会觉得 JS 坑,怎么能出现这种情况;其实问题不大,因为此时代码已经报错了,后面的代码想执行也没机会。

参考:

我用了两个月的时间才理解 let

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq449245884/xiaozhi

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant