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

论坛 期权论坛 期权     
大迁世界   2019-7-20 10:25   5758   0
译者:前端小智

原文:https://dmitripavlutin.com/variables-lifecycle-and-why-let-is-not-hoisted/
为了保证的可读性,本文采用意译而非直译。
提升是将变量或函数定义移动到作用域头部的过程,通常是
  1. var
复制代码
声明的变量和函数声明
  1. functionfun(){...}
复制代码

当 ES6 引入
  1. let
复制代码
(以及与
  1. let
复制代码
类似声明的
  1. const
复制代码
  1. class
复制代码
)声明时,许多开发人员都使用提升定义来描述如何访问变量。但是在对这个问题进行了更多的探讨之后,令我惊讶的是提升并不是描述
  1. let
复制代码
变量的初始化和可用性的正确术语。
ES6 为
  1. let
复制代码
提供了一个不同的和改进的机制。它要求更严格的变量声明,在定义之前不能使用,从而提高代码质量。
[h1]1. 容易出错的 var 提升[/h1]有时候我们会在zuo内作用域内看到一个奇怪的变量
  1. varvarname
复制代码
和函数函数function
  1. funName(){...}
复制代码
声明:
    1. // var hoisting
    复制代码
    1. num;     // => undefined
    复制代码
    1. var num;
    复制代码
    1. num = 10;
    复制代码
    1. num;     // => 10
    复制代码
    1. // function hoisting
    复制代码
    1. getPi;   // => function getPi() {...}
    复制代码
    1. getPi(); // => 3.14
    复制代码
    1. function getPi() {
    复制代码
    1.   return 3.14;
    复制代码
    1. }
    复制代码
变量
  1. num
复制代码
在声明
  1. varnum
复制代码
之前被访问,因此它被赋值为
  1. undefined
复制代码
  1. fucntion getPi(){…}
复制代码
在文件末尾定义。但是,可以在声明
  1. getPi()
复制代码
之前调用该函数,因为它被提升到作用域的顶部。
事实证明,先使用然后声明变量或函数的可能性会造成混淆。假设您滚动一个大文件,突然看到一个未声明的变量,它到底是如何出现在这里的,以及它在哪里定义的?
当然,一个熟练的JavaScript开发人员不会这样编写代码。但是在成千上万的JavaScript中,GitHub repos是很有可能处理这样的代码的。
即使查看上面给出的代码示例,也很难理解代码中的声明流。
当然,首先要声明再使用。
  1. let
复制代码
鼓励咱们使用这种方法处理变量。
[h1]2. 理解背后原理:变量生命周期[/h1]当引擎处理变量时,它们的生命周期由以下阶段组成:

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


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


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

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


请注意,就变量生命周期而言,声明阶段与变量声明是不同的概念。简而言之,JS引擎在3个阶段处理变量声明:声明阶段,初始化阶段和赋值阶段。
[h1]3.var 变量的生命周期[/h1]熟悉生命周期阶段之后,让我们使用它们来描述JS引擎如何处理
  1. var
复制代码
变量。


假设JS遇到一个函数作用域,其中包含
  1. var
复制代码
变量语句。变量在执行任何语句之前通过声明阶段,并立即通过作用域开始处的初始化阶段(步骤1)。函数作用域中
  1. var
复制代码
变量语句的位置不影响声明和初始化阶段。
在声明和初始化之后,但在赋值阶段之前,变量具有
  1. undefined
复制代码
的值,并且已经可以使用。
在赋值阶段
  1. variable='value'
复制代码
时,变量接收它的初值(步骤2)。
严格意义的提升是指在函数作用域的开始处声明并初始化一个变量。声明阶段和初始化阶段之间没有差别。
让我们来研究一个例子。下面的代码创建了一个包含
  1. var
复制代码
语句的函数作用域
    1. function multiplyByTen(number) {
    复制代码
    1.   console.log(ten); // => undefined
    复制代码
    1.   var ten;
    复制代码
    1.   ten = 10;
    复制代码
    1.   console.log(ten); // => 10
    复制代码
    1.   return number * ten;
    复制代码
    1. }
    复制代码
    1. multiplyByTen(4); // => 40
    复制代码
开始执行
  1. multipleByTen(4)
复制代码
并进入函数作用域时,变量
  1. ten
复制代码
在第一个语句之前通过声明和初始化步骤。因此,当调用
  1. console.log(ten)
复制代码
时,打印
  1. undefined
复制代码
。语句
  1. ten=10
复制代码
指定一个初值。赋值之后,
  1. console.log(ten)
复制代码
将正确地打印
  1. 10
复制代码

[h1]4. 函数声明生命周期[/h1]在函数声明语句
  1. functionfunName(){...}
复制代码
的情况下,它比变量声明生命周期更简单。


声明、初始化和赋值阶段同时发生在封闭函数作用域的开头(只有一步)。可以在作用域的任何位置调用
  1. funName()
复制代码
,而不依赖于声明语句的位置(甚至可以在末尾调用)。
下面的代码示例演示了函数提升:
    1. function sumArray(array) {
    复制代码
    1.   return array.reduce(sum);
    复制代码
    1.   function sum(a, b) {
    复制代码
    1.     return a + b;
    复制代码
    1.   }
    复制代码
    1. }
    复制代码
    1. sumArray([5, 10, 8]); // => 23
    复制代码
当执行
  1. sumArray([5,10,8])
复制代码
时,它进入
  1. sumArray
复制代码
函数作用域。在这个作用域内,在任何语句执行之前,
  1. sum
复制代码
都会通过所有三个阶段:声明、初始化和赋值。这样,
  1. array.reduce(sum)
复制代码
甚至可以在它的声明语句
  1. sum(a,b){…}
复制代码
之前使用
  1. sum
复制代码

[h1]5. let 变量的生命周期[/h1]
  1. let
复制代码
变量的处理方式与
  1. var
复制代码
不同,主要区别在于声明和初始化阶段是分开的。


现在来看看一个场景,当解释器进入一个包含
  1. let
复制代码
变量语句的块作用域时。变量立即通过声明阶段,在作用域中注册其名称(步骤1)。
然后解释器继续逐行解析块语句。
如果在此阶段尝试访问变量,JS 将抛出
  1. ReferenceError:variableisnotdefined
复制代码
。这是因为变量状态
  1. 未初始化
复制代码
,变量位于暂时死区 temporal dead zone。
当解释器执行到语句
  1. letvariable
复制代码
时,传递初始化阶段(步骤2)。变量退出暂时死区。
接着,当赋值语句
  1. variable='value'
复制代码
出现时,将传递赋值阶段(步骤3)。
如果JS 遇到
  1. letvariable='value'
复制代码
,那么初始化和赋值将在一条语句中发生。
让我们看一个例子,在块作用域中用
  1. let
复制代码
声明变量
  1. number
复制代码
    1. let condition = true;
    复制代码
    1. if (condition) {
    复制代码
    1.   // console.log(number); // => Throws ReferenceError
    复制代码
    1.   let number;
    复制代码
    1.   console.log(number); // => undefined
    复制代码
    1.   number = 5;
    复制代码
    1.   console.log(number); // => 5
    复制代码
    1. }
    复制代码
当 JS 进入
  1. if(condition){...}
复制代码
块作用域,
  1. number
复制代码
立即通过声明阶段。
由于
  1. number
复制代码
已经处于单一化状态,并且处于的暂时死区,因此访问该变量将引发
  1. ReferenceError:numberisnotdefined
复制代码
。接着,语句
  1. letnumber
复制代码
进行初始化。现在可以访问变量,但是它的值是
  1. undefined
复制代码
  1. const
复制代码
  1. class
复制代码
类型与
  1. let
复制代码
具有相同的生命周期,只是分配只能发生一次。
[h3]5.1 提升在
  1. let
复制代码
生命周期中无效的原因[/h3]如上所述,提升是变量在作用域顶部的耦合声明和初始化阶段。然而,
  1. let
复制代码
生命周期分离声明和初始化阶段。解耦消除了
  1. let
复制代码
的提升期限。
这两个阶段之间的间隙产生了暂时死区,在这里变量不能被访问。
[h1]总结[/h1]使用
  1. var
复制代码
声明变量很容易出错。在此基础上,ES6 引入了
  1. let
复制代码
。它使用一种改进的算法来声明变量,并附加了块作用域。
由于声明和初始化阶段是解耦的,提升对于
  1. let
复制代码
变量(包括
  1. const
复制代码
  1. class
复制代码
)无效。在初始化之前,变量处于暂时死区,不能访问。
为了保持变量声明的流畅性,建议使用以下技巧

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


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

[h1]番外[/h1][h3]如何理解 let x = x 报错之后,再次 let x 依然会报错?[/h3]

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

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


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


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


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

参考:
我用了两个月的时间才理解 let
[h1]交流[/h1]我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,即可看到福利,你懂的。
[h1]交延阅读[/h1]所以你真的懂JavaScript?
如何使用 Set 来提高代码的性能
Array.slice 8种不同用法
分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:20
帖子:4
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP