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)無效。在初始化以前,變量處於暫時性死區而且不能夠訪問。

保持穩定的變量聲明,有以下建議:

  • 聲明、初始化,而後再使用變量。這個流程正確而且容易遵照;
  • 儘量隱藏變量。變量暴露的越少,代碼就越模塊化。
相關文章
相關標籤/搜索