JavaScript变量的生命周期:为何let不被提高

原文连接:dmitripavlutin.com/variables-l…算法

提高其实是把变量和函数定义移动到做用域顶部的过程,一般发生在变量声明var或函数声明function fun() {...}bash

let(包括和let有一样声明行为的constclass)被ES2015提出来的时候,包括我在内的许多开发人员都使用提高来描述变量是如何被访问的。可是在对这个问题进行更多的搜索以后,让我惊讶的是,提高并非描述let变量初始化和可用性的正确术语。模块化

ES2015给let提供了不一样而且改善的机制。它要求更严格的变量声明实践(在定义以前不能使用),所以会有更高质量的代码。函数

咱们去看看这个过程当中更多的细节。ui

1.容易出错的var提高

有时候我会看见一个奇怪的实践,变量var varname和函数function funcName() {...}在做用域任意地方声明:spa

// 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声明以前被访问,因此被认定为undefinedcode

函数function getPi() {...}被定义在末尾。因为它被提高到做用域的顶部,因此函数能在定义getPi()以前被调用。cdn

这就是典型的提高。blog

事实证实,先使用后声明变量或函数可能会形成混乱。假设您在滚动查看一个大文件,忽然看到一个未声明的变量...它是怎么出如今这里,又是在哪里定义的?生命周期

固然一个经验丰富的JavaScript开发者不会用这种方式写代码,可是在成千上万的JavaScript GitHub仓库中颇有可能会面对这样的代码。

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

天然地,首先声明或描述一个未知的词语,而后才使用它来编写短语。let鼓励遵循这种方式来使用变量。

2.探索底层:变量的生命周期

当引擎访问变量的时候,它们的生命周期包括下面几个阶段:

  1. 声明阶段:在做用域中注册一个变量
  2. 初始化阶段:分配内存,给做用域中的变量建立绑定。在这个阶段,变量自动地被初始化为undefined
  3. 赋值阶段:给已经初始化过的变量赋值

经过声明阶段可是没有到达初始化阶段的变量是处于未定义的状态。

注意,根据变量的生命周期,声明阶段和通常说的变量声明是不一样的术语。简言之,引擎在三个阶段处理变量声明:声明阶段、初始化阶段和赋值阶段。

3.var变量的生命周期

熟悉了生命周期阶段,咱们用它们来描述引擎是怎样处理var变量的。

假设一个场景,当JavaScript遇到一个做用域里有一个 var声明的变量的函数。在任何语句执行以前,这个变量就经过了声明阶段和初始化阶段(第一步)。

var variable在函数做用域中声明的位置不影响声明和初始化阶段。

在声明和初始化以后,赋值阶段以前,变量值为undefined而且能够被访问使用。

在赋值阶段,variable = 'value',变量会获得它的初始值(第二步)。

严格来讲,提高的概念是在函数做用域的顶部声明和初始化变量。在声明和初始化阶段之间没有间隙。

来研究一个例子。下面的代码建立了一个带有var变量的函数:

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

当JavaScript开始执行multipleByTen(4)的时候,它就进入了multipleByTen的函数做用域,在第一条语句以前,变量ten完成了声明和初始化阶段。因此调用console.log(ten)会打印出undefined

语句ten = 10分配了一个初始值。分配以后,console.log(ten)正确地输出了10。

4.函数声明的生命周期

假设一个函数声明语句function funName() {...},这更加容易。

声明、初始化、赋值阶段在函数做用域的开始马上执行(只有一步)。 funcName()能够在该做用域的任何地方调用,不依赖于声明语句的位置(甚至能够在最后)。

下面的代码示例展现了函数提高:

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

当JavaScript调用sumArray([5, 10, 8])的时候,它就进入了sumArray的函数做用域。在做用域里面,在全部语句执行以前,sum经过了三个阶段:声明、初始化、赋值。 这种方式,array.reduce(sum)甚至能够在声明语句function sum(a, b) {...}以前使用sum

5.let变量的生命周期

let变量的处理方式和var不一样。最主要的区别就是声明和初始化阶段被分开了。

如今假设解释器进入了一个块级做用域,做用域包含一个 let variable语句。变量马上经过了声明阶段,在做用域注册了它的名字(步骤 1)。

接着解释器一行一行的解析语句。

若是在这个阶段尝试访问变量variable,JavaScript会抛出ReferenceError: variable is not defined。由于变量的状态是未定义的,variable在暂时性死区。

当解释器到达let variable语句的时候,初始化阶段经过(步骤 2)。如今变量的状态是已定义而且访问它会获得undefined

变量退出了暂时性死区。

稍后,当赋值语句variable = 'value'出现,就经过了赋值阶段(步骤 3)。

若是JavaScript遇到let variable = 'value',那么初始化和赋值会发生在这一条语句上。

来看一个例子。在块级做用域里面用let声明一个变量variable

let condition = true;  
  // console.log(number); // => Throws ReferenceError
  let number;
  console.log(number); // => undefined
  number = 5;
  console.log(number); // => 5
}
复制代码

当JavaScript进入if (condition) {...}块级做用域的时候,number马上经过了声明阶段。 由于number处于未定义的状态,处于暂时性死区,试图访问这个变量会抛出ReferenceError: number is not defined

后来,语句let number完成了初始化。如今这个变量能够访问了,可是它的值是undefined

赋值语句number = 5固然就是完成了赋值阶段。

constclass类型和let有相同的生命周期,除了赋值只能发生一次。

5.1 为何提高在let的生命周期里无效

如上所述,提高就是变量在做用域顶部进行声明和初始化。可是let的生命周期将声明和初始化两个阶段解耦了。解耦让提高这个术语失效了。

两个阶段中间的间隙建立了暂时性死区,在这里,变量不能被访问。

以一种科幻的风格,在let生命周期中,提高这个术语的崩塌创造了暂时性死区。

6.结论

随意使用var声明变量容易犯错。基于这个教训,ES2015建立了let。它使用一种改进的算法来声明变量,而且使用块级做用域。

因为声明和定义两个阶段解耦,提高对于一个let声明的变量(包括包括constclass)无效。在初始化以前,变量处于暂时性死区而且不能够访问。

保持稳定的变量声明,有以下建议:

  • 声明、初始化,而后再使用变量。这个流程正确而且容易遵照;
  • 尽量隐藏变量。变量暴露的越少,代码就越模块化。
相关文章
相关标签/搜索