JavaScript 變量的生命周期:為什么 let 不存在變量提升
變量提升是一個將變量或者聲明函數提升到作用域起始處的過程,通常指的是變量聲明 var
和函數聲明 function fun() {...}
當 let
(以及具備了和 let
相似聲明行為的 const
和 class
)等聲明方式在 ES2015 中被引入后,許多的開發者包括我都使用了變量提升的定義來描述變量是如何被訪問的。但經過對這個問題更多的搜索后,我十分驚訝的發現變量提升并不是可以用來準確描述 let
變量初始化和可用性的合適術語。
ES2015 為 let
提供了一個不同的改進機制。它要求了更嚴格的變量聲明方式(你在定義變量前是無法訪問它的)并且這也在結果上保證了更好的代碼質量。
現在讓我們一起深入了解關于這個過程的更多細節。
1. 容易出錯的 var
變量提升
有時我會在作用域下的任何位置上看到一個奇怪的變量聲明 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
之前就被訪問了,所以它的值為 undefined
函數 function getPi() {...}
是定義在文件的末尾的。然而函數可以在它聲明 getPi()
之前就被調用,因為它被提升到了作用域的頂部。
這就是典型的變量提升。
事實證明,在首次使用變量或函數后才聲明變量或函數會很容易產生困惑。假設你正滾動查看一個大文件,然后發現了一個未聲明的變量...你肯定會想它到底為什么在這里出現并且它是在哪定義的呢?
當然一個熟練的 JavaScript 開發者并不會這樣編寫代碼。但在成千上萬個 JavaScript Github 庫中卻可能存在著相當數量的這樣的代碼。
甚至在上面給出的代碼示例中,我們也很難去明白代碼中的聲明流程。
我們應當自然地首先聲明或是描述一個未知的術語。在這之后再對它進行使用。let
便是鼓勵你遵循這種方法來設置變量。
2. 深層內容: 變量的生命周期
當引擎使用變量時,它們的生命周期包含以下階段:
-
聲明階段 這一階段在作用域中注冊了一個變量。
-
初始化階段 這一階段分配了內存并在作用域中讓內存與變量建立了一個綁定。在這一步變量會被自動初始化為
undefined
。 -
賦值階段 這一階段為初始化變量分配具體的一個值。
一個變量在通過聲明階段時它還是處于 未初始化的 狀態,這時它仍然還沒有到達初始化階段。
注意,按照變量的生命周期過程,聲明階段與我們通常所說的變量聲明是不同的術語。簡單來講,引擎處理變量聲明需要經過完整的這 3 個階段:聲明階段,初始化階段和賦值階段。
3. var
變量的生命周期
稍微熟悉下這些生命周期階段,現在讓我們用它們來描述引擎是如何處理 var
變量的。
假設一個場景,當 JavaScript 遇到了一個函數作用域,其中包含了 var variable
的語句。則在任何語句執行之前,這個變量在作用域的開頭就通過了聲明階段并馬上來到了初始化階段(步驟一)。
同時 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)
時進入了函數作用域中,變量 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
當 JavaScript 執行 sumArray([5, 10, 8])
時,它便進入了 sumArray
的函數作用域。在作用域內,任何語句執行之前的瞬間,sum
就經過了所有的三個階段:聲明,初始化和賦值階段。
這樣 array.reduce(sum)
即使在它的聲明語句 function sum(a, b) {...}
之前也可以使用 sum
。
5. let
變量的生命周期
let
變量的處理方式不同于 var
。它的主要區分點在于聲明和初始化階段是分開的。
現在讓我們研究這樣一個場景,當解釋器進入了一個包含 let variable
語句的塊級作用域中。這個變量立即通過了聲明階段,并在作用域內注冊了它的名稱(步驟一)。
然后解釋器繼續逐行解析塊語句。
這時如果你在這個階段嘗試訪問 variable
,JavaScript 將會拋出 ReferenceError: variable is not defined
。因為這個變量的狀態依然是未初始化的。
此時 variable
處于臨時死區中。
當解釋器到達語句 let variable
時,此時變量通過了初始化階段(步驟二)。現在變量狀態是初始化的并且訪問它的值是 undefined
。
同時變量在此時也離開了臨時死區。
之后當到達賦值語句 variable = 'value'
時,變量通過了賦值階段(步驟三)。
如果 JavaScript 遇到這樣的語句 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
}
當 JavaScript 進入 if (condition) {...}
塊級作用域中,number
立即通過了聲明階段。
因為 number
尚未初始化并且處于臨時死區,此時試圖訪問該變量會拋出 ReferenceError: number is not defined
.
之后語句 let number
使其得以初始化。現在變量可以被訪問,但它的值是 undefined
。
之后賦值語句 number = 5
當然也使變量經過了賦值階段。
const
和 class
類型與 let
有著相同的生命周期,除了它們的賦值語句只會發生一次。
5.1 為什么變量提升在 let
的生命周期中無效
如上所述,變量提升是變量的耦合聲明并且在作用域的頂部完成初始化。
然而 let
生命周期中將聲明和初始化階段解耦。這一解耦使 let
的變量提升現象消失。
由于兩個階段之間的間隙創建了臨時死區,在此時變量無法被訪問。
這就像科幻的風格一樣,在 let
生命周期中由于變量提升失效所以產生了臨時死區。
6. 結論
使用 var
自由的去聲明變量很容易出現錯誤。
基于這一點,ES2015 引進了 let
。它使用了一種改進的算法來聲明變量并添加了塊作用域。
因為聲明和初始化階段是解耦的,變量提升對于 let
變量(也包括 const
和 class
)是無效的。在初始化之前,變量處于臨時死區中并不可被訪問。
為了保證平穩的變量聲明,推薦這些技巧以供參考:
-
聲明,初始化變量后再使用變量。這個流程才是正確并易于遵循的。
-
盡可能的減少變量數。你暴露的變量越少,你的代碼則會變得更加模塊化。
這就是今天所有的內容。我們在下一篇文章再見。
Save
來自:http://www.zcfy.cc/article/javascript-variables-lifecycle-why-let-is-not-hoisted-976.html