深刻淺出javascript (3)—— let 和 const

深刻淺出javascript (1)—— 變量提高中咱們知道使用var 聲明變量常常會有意想不到的效果,所以在ES6中引入了塊級做用域以及 let、const 關鍵字來規避這種狀況。今天咱們就來講說。javascript

做用域

想要講明白 JavaScript 的變量提高這個特性,咱們須要先從做用域講起。java

做用域是一塊在程序中定義變量的區域,該區域決定了變量的生命週期。換句話說,做用域就是變量與函數的可訪問範圍,它控制着變量和函數的可見性和生命週期。segmentfault

在 ES6 以前,ES 的做用域只有兩種:全局做用域和函數做用域。函數

  • 全局做用域中的對象在代碼中的任何地方都能訪問,其生命週期伴隨着頁面的生命週期。
  • 函數做用域就是在函數內部定義的變量或者函數,而且定義的變量或者函數只能在函數內部被訪問。函數執行結束以後,函數內部定義的變量會被銷燬。

而其餘語言基本都支持塊級做用域。塊級做用域就是使用一對大括號包裹的一段代碼,好比函數、判斷語句、循環語句,甚至單獨的一個{}均可以被看做是一個塊級做用域。spa

例以下面的代碼都是塊級做用域:設計

//if塊 
if(1){} 
//while塊 
while(1){} 
//函數塊 
function foo(){} 
//for循環塊 
for(let i = 0; i<100; i++){} 
//單獨一個塊 
{}

對於塊級做用域最重要的就是其代碼塊內部定義的變量在代碼塊外部是訪問不到的,而且等該代碼塊中的代碼執行完成以後,代碼塊中定義的變量會被銷燬。
JavaScript 語言設計之初並無引入塊級做用域的概念,因而把做用域內部的變量統一提高無疑是最快速、最簡單的設計,因此纔有了 var 的變量提高。指針

變量提高所帶來的問題

1. 變量容易在不被察覺的狀況下被覆蓋掉

咱們看這種狀況:code

var myname = "wens" 
function showName(){ 
    console.log(myname); 
    if(0){ 
        var myname = "leon" 
    } 
    console.log(myname); 
} 
showName()

執行這段代碼,打印出來的是 undefined,而並不會像具備塊級做用域那樣的語音同樣打印出來「wens」的字符串。至於爲何輸出的內容是 undefined ?相信看過我上一篇文章的同窗必定能知道答案對象

2. 本應銷燬的變量沒有被銷燬

接下來咱們再來看下面這段讓人誤解更大的代碼:blog

function foo(){ 
    for (var i = 0; i < 7; i++) { } 
    console.log(i); 
} 
foo()

若是咱們使用有塊級做用域的語音,在 for 循環結束以後,i 就已經被銷燬了,可是在 JavaScript 代碼中,i 的值並未被銷燬,因此最後打印出來的是 7。

這一樣也是由變量提高而致使的,在建立執行上下文階段,變量 i 就已經被提高了,因此當 for 循環結束以後,變量 i 並無被銷燬。

let 和 const

上面咱們介紹了變量提高而帶來的一系列問題,爲了解決這些問題,ES6 引入了 let 和 const 關鍵字,從而使 JavaScript 也能像其餘語言同樣擁有了塊級做用域。

關於 let 和 const 的用法,參考下面代碼:

let x = 5 
const y = 6 
x = 7 
y = 9 //報錯,const聲明的變量不能夠修改

從這段代碼你能夠看出來,二者之間的區別是,使用 let 關鍵字聲明的變量是能夠被改變的,而使用 const 聲明的變量其值是不能夠被改變的(對於引用類型的值能夠修改堆內存中那個值,可是不能修改指針的指向)。但無論怎樣,二者均可以生成塊級做用域,爲了簡單起見,在下面的代碼中,我統一使用 let 關鍵字來演示。

那麼接下來,咱們就經過實際的例子來分析下,ES6 是如何經過塊級做用域來解決上面的問題的。

你能夠先參考下面這段存在變量提高的代碼:

function varTest() { 
    var x = 1; 
    if (true) { 
        var x = 2; // 一樣的變量! 
        console.log(x); // 2
    } 
    console.log(x); // 2 
}

在這段代碼中,有兩個地方都定義了變量 x,第一個地方在函數塊的頂部,第二個地方在 if 塊的內部,因爲 var 的做用範圍是整個函數,因此在編譯階段,會生成以下的執行上下文:
image.png

從執行上下文的變量環境中能夠看出,最終只生成了一個變量 x,函數體內全部對 x 的賦值操做都會直接改變變量環境中的 x 值。

因此上述代碼最後經過 console.log(x) 輸出的是 2,而對於相同邏輯的代碼,有塊級做用域的語言最後一步輸出的值應該是 1,由於在 if 塊裏面的聲明不該該影響到塊外面的變量。
下面咱們使用 let 關鍵字替換 var 關鍵字使其具有塊級做用域,改造後的代碼以下:

function varTest() { 
    let x = 1; 
    if (true) { 
        let x = 2; // 一樣的變量! 
        console.log(x); // 2
    } 
    console.log(x); // 2 
}

執行這段代碼,其輸出結果就和咱們的預期是一致的。

那麼,JavaScript 是如何在沒有破壞變量提高的狀況下還支持塊級做用域呢?
下面咱們經過一段代碼來講明:

function foo(){ 
    var a = 1 
    let b = 2 
    { 
        let b = 3 
        var c = 4 
        let d = 5 
        console.log(a) 
        console.log(b) 
    } 
    console.log(b) 
    console.log(c) 
    console.log(d)
}
foo()

讀過前面兩篇文章的同窗都知道,當執行上面這段代碼的時候,JavaScript 引擎會先對其進行編譯並建立執行上下文,而後再按照順序執行代碼。可是如今的狀況有點不同,咱們引入了 let 關鍵字這就有了塊級做用域。下面是執行過程:

第一步是編譯並建立執行上下文:
image.png

經過上圖,咱們能夠得出如下結論:

  • 函數內部經過 var 聲明的變量,在編譯階段全都被存放到變量環境裏面了。
  • 經過 let 聲明的變量,在編譯階段會被存放到詞法環境(Lexical Environment)中。
  • 在函數的做用域塊內部,經過 let 聲明的變量並無被存放到詞法環境中。

接下來,第二步繼續執行代碼,當執行到代碼塊裏面時,變量環境中 a 的值已經被設置成了 1,詞法環境中 b 的值已經被設置成了 2,這時候函數的執行上下文就以下圖所示:

image.png

從圖中能夠看出,當進入函數的做用域塊時,做用域塊中經過 let 聲明的變量,會被存放在詞法環境的一個單獨的區域中,這個區域中的變量並不影響做用域塊外面的變量,好比在做用域外面聲明瞭變量 b,在該做用域塊內部也聲明瞭變量 b,當執行到做用域內部時,它們都是獨立的存在。

其實,在詞法環境內部,維護了一個小型棧結構,棧底是函數最外層的變量,進入一個做用域塊後,就會把該做用域塊內部的變量壓到棧頂;看成用域執行完成以後,該做用域的信息就會從棧頂彈出,這就是詞法環境的結構。

再接下來,當執行到做用域塊中的console.log(a)這行代碼時,就須要在詞法環境和變量環境中查找變量 a 的值了,具體查找方式是:沿着詞法環境的棧頂向下查詢,若是在詞法環境中的某個塊中查找到了,就直接返回給 JavaScript 引擎,若是沒有查找到,那麼繼續在變量環境中查找(一個完整的查找變量和函數的過程會涉及到做用域鏈,這個咱們在下篇文章會介紹)。

看成用域塊執行結束以後,其內部定義的變量就會從詞法環境的棧頂彈出,最終執行上下文以下圖所示:

image.png

經過上面的分析,想必你已經理解了詞法環境的結構和工做機制,塊級做用域就是經過詞法環境的棧結構來實現的,而變量提高是經過變量環境來實現,經過這二者的結合,JavaScript 引擎也就同時支持了變量提高和塊級做用域了。

總結

今天講解的內容就結束了,下面我來簡單總結下今天的內容。

因爲 JavaScript 的變量提高存在着變量覆蓋、變量污染等設計缺陷,因此 ES6 引入了塊級做用域關鍵字來解決這些問題。

咱們經過對變量環境和詞法環境的介紹,分析了 JavaScript 引擎是如何同時支持變量提高和塊級做用域的。

下一篇文章咱們來說解一下做用域鏈。

相關文章
相關標籤/搜索