JavaScript變數的生命週期:為什麼let不被提升

我很可愛你信不信發表於2018-03-12

原文連結:dmitripavlutin.com/variables-l…

提升實際上是把變數和函式定義移動到作用域頂部的過程,通常發生在變數宣告var或函式宣告function fun() {...}

let(包括和let有同樣宣告行為的constclass)被ES2015提出來的時候,包括我在內的許多開發人員都使用提升來描述變數是如何被訪問的。但是在對這個問題進行更多的搜尋之後,讓我驚訝的是,提升並不是描述let變數初始化和可用性的正確術語。

ES2015給let提供了不同並且改善的機制。它要求更嚴格的變數宣告實踐(在定義之前不能使用),因此會有更高質量的程式碼。

我們去看看這個過程中更多的細節。

1.容易出錯的var提升

有時候我會看見一個奇怪的實踐,變數var varname和函式function funcName() {...}在作用域任意地方宣告:

// 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宣告之前被訪問,所以被認定為undefined

函式function getPi() {...}被定義在末尾。由於它被提升到作用域的頂部,所以函式能在定義getPi()之前被呼叫。

這就是典型的提升。

事實證明,先使用後宣告變數或函式可能會造成混亂。假設您在滾動檢視一個大檔案,突然看到一個未宣告的變數...它是怎麼出現在這裡,又是在哪裡定義的?

當然一個經驗豐富的JavaScript開發者不會用這種方式寫程式碼,但是在成千上萬的JavaScript GitHub倉庫中很有可能會面對這樣的程式碼。

即使看著上面給出的程式碼示例,也很難理解程式碼中的宣告流程。

自然地,首先宣告或描述一個未知的詞語,然後才使用它來編寫短語。let鼓勵遵循這種方式來使用變數。

2.探索底層:變數的生命週期

當引擎訪問變數的時候,它們的生命週期包括下面幾個階段:

  1. 宣告階段:在作用域中註冊一個變數
  2. 初始化階段:分配記憶體,給作用域中的變數建立繫結。在這個階段,變數自動地被初始化為undefined
  3. 賦值階段:給已經初始化過的變數賦值

通過宣告階段但是沒有到達初始化階段的變數是處於未定義的狀態。

JavaScript變數的生命週期:為什麼let不被提升

注意,根據變數的生命週期,宣告階段和一般說的變數宣告是不同的術語。簡言之,引擎在三個階段處理變數宣告:宣告階段、初始化階段和賦值階段。

3.var變數的生命週期

熟悉了生命週期階段,我們用它們來描述引擎是怎樣處理var變數的。

JavaScript變數的生命週期:為什麼let不被提升
假設一個場景,當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() {...},這更加容易。

JavaScript變數的生命週期:為什麼let不被提升
宣告、初始化、賦值階段在函式作用域的開始立刻執行(只有一步)。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不同。最主要的區別就是宣告和初始化階段被分開了。

JavaScript變數的生命週期:為什麼let不被提升
現在假設直譯器進入了一個塊級作用域,作用域包含一個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)無效。在初始化之前,變數處於暫時性死區並且不可以訪問。

保持穩定的變數宣告,有如下建議:

  • 宣告、初始化,然後再使用變數。這個流程正確並且容易遵守;
  • 儘可能隱藏變數。變數暴露的越少,程式碼就越模組化。

相關文章