塊級做用域綁定(臨時死區)(var && let && const 的區別和聯繫)

導語

過去, javascript 的變量聲明機制一直令咱們感到困惑。 大多數類c語言在聲明變量的同時也會建立變量(綁定)。 而在之前的javascrpit 中, 什麼時候建立變量要看怎麼聲明變量。 es6 的新語法能夠幫助你更好地控制做用域. 本文將解釋爲何經典的var 容易讓人迷惑 。javascript

1 .var 聲明&提高機制(Hoisting)

在函數做用域或全局做用域中經過關鍵字var 聲明的變量 , 不管其實是在哪裏聲明的 , 都會被當成在當前做用域頂部聲明的變量 , 這就是咱們常說的提高機制<br>
例如:<br>

```function getValue () {
    if(0) {
        var value = 'blue';
        return value ;
    } else {
        return null
    }
}
`
複製代碼

解釋 : 若是你不熟悉javascript , 可能會認爲只有當condition的值爲true 時纔會建立變量value 。 事實上, 不管如何變量 value 都會被建立。 在預編譯階段, js 引擎會將上面的getValue函數修改爲下面這樣:java

`function getValue () {
    var value ; 
    if(0) {
        value = 'blue' ;
        return value ;
    } else {
        return null ;
    }
}
複製代碼

變量聲明被提高至函數做用域頂部, 而初始化操做依舊留在原處執行, 這就意味着在else子句中也能夠訪問到該變量 , 並且因爲此時變量還沒有初始化 , 因此其值爲undefined.node


2. 塊級聲明

塊級聲明用於聲明在指定塊的做用域以外沒法訪問的變量 , 塊級做用域存在於 :es6

  • 函數內部瀏覽器

  • 塊中{} 不少類c語言都有塊級做用域 , 而es6 引入就上爲了讓js 更靈活和普適安全

    let 聲明

    let 聲明的用法與var 相同。 用let 代替var來聲明變量, 就能夠把變量的做用域限制在當前代碼塊中 , (稍後咱們將在臨時死區一節中討論另外幾處細微的語法差別), 因爲let 聲明不會被提高, 所以開發者將let 聲明語句放在封閉代碼塊的頂部 , 以便整個代碼塊均可以訪問, 例如 :bash

    function getValue (condition) {
        if(condition) {
            let value = 'blue' ;
            return value ;
        } else {
            //變量value 不存在
            return null
        }
        //變量value 在此處不存在
    }
    複製代碼

如今這個getValue 函數的運行結果更像類c語言. 變量value改由關鍵字let 進行聲明後 , 再也不被提高至函數頂部。 執行流離開if 塊value馬上被銷燬。 若是condition 的值false ,就永遠不會聲明並初始化value.函數

禁止重聲明

假設做用域中已經存在某個標誌符, 此時再使用let 關鍵字聲明它就會報錯 , 好比:ui

var  count = 30 ;
//拋出錯誤
let count = 10 ;
複製代碼

解釋:同一做用域中, 不能用let 重複定義已經存在的標誌符, 因此此處的let 聲明會拋出錯誤。但若是當前做用域內嵌另外一個做用域, 即可在內嵌的做用域中用let 聲明同名變量,例如 :spa

var count = 30 ; 
if(1) {
    //不會拋出錯誤
    let count = 10 ;
}
複製代碼

因爲此處的let是在if塊內聲明瞭新變量count, 所以不會拋出錯誤。 內部塊中的count會遮蔽全局做用域中的count , 後者只有在if塊外才能訪問到

const 聲明

es6標準還提供了const關鍵字。使用const 聲明的變量是常量,其值一旦被設定後不可更改。所以,每一個經過const聲明的常量必須進行初始化,例如

// 有效的常量
const maxItem = 30 ;

//語法錯誤 , 常量未初始化
const name ;
複製代碼

const與let

相同點
const 與let 聲明的都是塊級標識符,

  1. 因此常量也只在當前代碼塊內有效,一旦執行到塊外會當即被銷燬
  2. 在同一做用域用const 聲明已經存在的標識符也會致使語法錯誤,不管該標誌符是使用var 仍是let聲明的,好比
    var message = 'hello' ;
    let age = 25 ; 
    
    //這倆條都會拋出錯誤
    const message = 'goodbye' ;
    const age = 30
    
    複製代碼

後倆條const 聲明語句自己沒問題, 但因爲前面用var 和let 聲明瞭倆個同名變量,結果代碼就沒法執行了
儘管類似之處不少, 但有一個很大的不一樣,即不管是在嚴格模式下仍是非嚴格模式下,都不能夠爲const定義的常量再賦值, 不然會拋出錯誤

const maxItems = 5 ;
//拋出錯誤
maxItem = 6 ;
複製代碼

然而js與其餘語言不一樣的是, javascript中的變量若是是對象,則對象中的值能夠修改

用const 聲明對象

const聲明不容許修改綁定, 但容許修改值,好比

const person = {
    name: 'lihua'
}
//能夠修改對象屬性的值
person.name = 'hanmeimei'

//拋出錯誤
person = {
    name:'hanmeimei'
}
複製代碼

切記:若是直接給person賦值, 即要改變person的值, 就會拋出錯誤。const聲明不容許修改綁定,但容許修改值

臨時死區(Temporal Dead Zone)

與var 不一樣,let 和 const 聲明的變量不會被提高到做用域頂部, 若是在聲明以前訪問這些變量, 即時是相對安全的typeof操做符也會觸發引用錯誤,例如:

if(1) {
    console.log(typeof value) ;//引用錯誤
    let value = 'blue'
}
複製代碼

因爲console.log(typeof value)語句會拋出錯誤, 所以用let定義並初始化變量value的語句不會執行。此時的value還位於js所謂的臨時死區(TDZ)中。雖然es標準並無明確提到TDZ,但人們經常使用來它來描述let和const的不提高效果。
解釋:javaScript引擎在掃描代碼發現變量聲明中,要麼將他們提高至做用域頂部(遇到var 聲明),要麼將聲明放到TDZ中(遇到let 和const聲明)。訪問TDZ中的變量會觸發運行時錯誤。只有執行過變量聲明的語句後,變量纔會從TDZ中移出,而後方可正常訪問。
在聲明前訪問由let定義的變量就是這樣。由前面實例可見,即使是相對不容易出錯的typeof操做符也沒法阻擋引擎拋出的錯誤。但在let聲明的做用域外對該變量使用typeof則不會報錯,具體以下:

console.log(typeof value) ;//undefined
    
    if(1) {
        let value = 'blue'
    }
複製代碼

typeof 是在聲明value的代碼塊外執行的,此時value並不在TDZ中。這也就意味着不存在value這個綁定,typeof操做最終返回undefined

循環塊級做用域綁定

在下文中,一切實例的運行環境均是瀏覽器 , 而不是node,因此有可能出如今node上不合適的狀況
開發者可能最但願實現for循環的塊級做用域了,由於能夠把隨意聲明的計數器變量限制在循環內部。例如:

for (var i = 0 ; i < 10 ; i ++) {
    process(item[i]) ;
}
//這裏仍然能夠訪問變量i
console.log(i) ; // 10
複製代碼

在默認擁有塊級做用域的其餘語言中,這個實例也能夠正常運行, 而且變量i只有在for循環中才能訪問到。而在javascript中,因爲var 聲明獲得了提高,變量i在循環結束後仍然可訪問到。若是換用let聲明變量就能獲得想要的結果,好比:

for (let i = 0 ; i < 10  ; i ++) {
    process(item[i]) ;
}

//i在這裏不可訪問,拋出一個錯誤
console.log(i) ;
複製代碼

解釋 :在這個實例中, 變量i只存在於for循環中,一旦循環結束,在其餘地方均沒法訪問這個變量

循環中的函數

長久以來, var聲明讓開發者在循環中建立函數變得異常困難,由於變量到了循環以外仍能訪問。請看這段代碼:

var funcs = [] ;

for (var i = 0 ; i < 10 ; i ++) {
    funcs.push(function (){
        console.log(i) ;
    }) ;
}

funcs.forEach ( (func) => {
    func() ; //輸出 10 次 數字 10
})
複製代碼

你預期的結果多是輸出數字 0 - 9 , 但它輸出了一連串的10次10 。這是由於循環裏的每次迭代同時共享着變量i , 循環內部建立的函數全都保留了對相同變量的引用。循環結束時變量i的值爲10, 因此每次調用console.log(i) 時就會輸出數字10

解決方法1: 自執行函數

爲了解決這個問題,開發者們在循環中使用當即執行函數表達式,以強制生成計數器變量的副本,就像這樣:

var funcs = [] ;

for(var i = 0 ; i < 10 ; i ++) {
    funcs.push((function(value) {
        return function() {
            console.log(value)
        }
    }(i)) ;
}

funcs.forEach(function (func) {
    func() ; // 輸出0 而後1 ,2 , ....  到 9 
}) ; 
複製代碼

解釋: 在循環內部自執行函數爲接受的每個變量i都建立了一個副本並存儲爲變量value。這個變量的值就是相應迭代建立的函數所使用的值。所以調用每一個函數都會像0到9循環同樣獲得指望的值。es6中的let和const提供的塊級綁定讓咱們無須這麼折騰

解決方法2: let && const

let聲明模仿上述實例中自執行函數所作的一切來簡化循環過程。每次迭代循環都會建立一個新變量,並以以前迭代中同名變量的值將其初始化。這意味着你完全刪除自執行函數以後仍然能夠獲得預期的結果,就像這樣:

var funcs = [] ;

for(let i = 0 ; i < 10 ; i++) {
    funcs.push(function(){
        console.log(i) ;
    })
}

funcs.forEach( func =>{
    func() ; //輸出 0 1  2  ... 9
})
複製代碼

這段循環與以前那段結合了var和自執行函數的循環的運行結果相同,但相比之下更爲簡潔。每次循環的時候let聲明都會建立一個新變量i,並將其初始化爲i的當前值。因此循環內部建立的每一個函數都能獲得屬於他們本身的i的副本。對於for-in 和 for-of 來講也是同樣的。
let聲明在循環內部的行爲是標準中專門定義的,它不必定與let的不提高特性有關,理解這一點相當重要。事實上,早期的let實現不包含這一行爲,它是後來加入的

全局塊做用域綁定

let和const與var的另一個區別是它們在全局做用域中的行爲。當var被用於全局做用域時,它會建立一個新的全局變量做爲全局對象(瀏覽器環境中的window對象)的屬性。這意味着用var 極可能會無心中覆蓋一個已經存在的全局變量,就像這樣:

//在瀏覽器中
var RegExp = "hello!" ;
console.log(window.RegExp) ;  // "hello"

var ncz = "Hi!" ;
console.log(window.ncz) ; // "Hi!"
複製代碼

即便全局對象RegExp定義在window上,也不能倖免被var聲明覆蓋。實例中聲明的全局對象RegExp會覆蓋以前原來那個。一樣,ncz被定義爲一個全局變量,並當即成爲window的屬性。javaScript過去一直這樣。
若是你在全局做用域中使用let或const,會在全局做用域下建立一個新的綁定,但該綁定不會添加爲全局對象的屬性。換句話說,用let或const不能覆蓋全局變量,而只能遮蔽它。實例以下:

//在瀏覽器中
let RegExp = "hello!" ;
console.log(RegExp) ;    //  "hello"
console.log(RegExp === window.RegExp)  //   false

const  ncz = "Hi!" ;
console.log(ncz) ;   //  "Hi!" 
console.log("ncz" in window)  //  false
複製代碼

這裏let聲明的RegExp建立了一個綁定並遮蔽了全局的RegExp變量。結果是window.RegExp和RegExp不相同,但不會破壞全局做用域。一樣,const聲明的ncz建立了一個綁定但沒有建立爲全局對象的屬性。若是不想爲全局對象建立屬性,則使用let和const要安全得多。
Note:若是但願在全局對象下定義變量,仍然可使用var。這種狀況常見於在瀏覽器中跨frame或跨window訪問代碼。

小結

塊級做用域綁定的let和const爲javaScript引入了詞法做用域,他們聲明的變量不會提高,並且只能夠在聲明這些變量的代碼塊中使用。如此一來,javascript聲明變量的語法與其餘語言就更像了。同時也下降了產生錯誤的可能。與此同時,在聲明前訪問塊級變量會致使錯誤,由於變量還在臨時死區(TDZ)中。
let和const的行爲很時候與var一致。然而,他們在循環中的行爲很不同。在for循環中,let和const都會每次迭代建立新綁定,從而使循環體內建立的函數能夠訪問到相應迭代的值,而非最後一次迭代後的值(像var聲明那樣)。

關於做者

本文部分借鑑了其餘大佬的做品(哈哈),只是爲了分享和複習,侵權立刪

相關文章
相關標籤/搜索