javascript中var、let、const聲明的區別

我在上一篇文章javascript中詞法環境、領域、執行上下文以及做業詳解中的最後稍微提到了有關var、let、const聲明的區別,在本篇中我會重點來分析它們之間到底有什麼不一樣。javascript

提到var、let、const中的區別不少人一會兒就想到了,var聲明的變量是全局或者整個函數塊的而let、const聲明的變量是塊級的變量。var聲明的變量存在變量提高,let、const聲明的變量不存在變量提高。let聲明的變量容許從新賦值,const聲明的變量不容許從新賦值。那麼它們之間真的只有這麼一點區別嗎,咱們先來看下面一個例子:html

注:本篇文章中的全部例子都以最新版chrome瀏覽器爲標準(低版本瀏覽器實現會有區別)。java

//咱們看一下這三句話,你認爲會發生什麼
let let = 1;
console.log(let);
//
const let = 1;
console.log(let);
//
var let = 1;
console.log(let);

不少人會認爲,let是關鍵字,上面這三句聲明都會報錯。可事實真的是這樣嗎?不是。let、const的聲明會報錯,可是var聲明被認爲是規範的,更重要的是let、const聲明報錯的緣由也不是由於let是關鍵詞而是因爲ECMAScript語言規範中規定了當用let、const聲明時若是標識符是let則報錯。chrome

該代碼是運行在非嚴格模式下的,嚴格模式則報錯,值得注意的是嚴格模式下上面三句話都是由於標識符let是保留字而報錯的。有興趣能夠在嚴格模式和非嚴格模式下測試let let = 1;報錯緣由是不一樣的。segmentfault

下面的全部代碼都在非嚴格模式下進行,若是是嚴格模式我會明確指出。數組

那麼上面三句話中的標識符let改成const會怎麼樣?不管是嚴格模式仍是非嚴格模式都報錯,錯誤緣由是由於const是關鍵字,這時候問題又來了,爲何標識符let和const的行爲會不一樣呢?這個鍋說到底仍是得ES5規範背,在ES5規範中const被認爲是將來保留字(FutureReservedWords)而let只有在嚴格模式下才被認爲是將來保留字,這致使var能夠聲明let卻不能聲明const,那到了ES6時代爲何不改呢?哎!不是不改而是心有力而餘不足啊,鬼知道在ES6時代以前有多少代碼中出現過var let這個聲明啊,這要是改了得有多少網站得炸啊。瀏覽器

基於上面的緣由,你看到下面的代碼時不要驚訝:閉包

var let = 1;
console.log(let);                  //1
let a = 2;
console.log(a);                   //2
//看着怪異可是徹底能夠工做,不會有任何錯誤

看完上面一個不一樣點,咱們再看下面這個例子:函數

var a;
console.log(a);                    //undefined
//
let a;
console.log(a);                    //undefined
//
const a;
console.log(a);                    //?

咱們都知道若是var和let只聲明變量而不賦值,那麼默認賦值undefined,那麼const會怎樣呢?
你在Chrome控制檯上試一下就知道了,語法錯誤缺乏初始化,ES6規範指出const聲明的標識符必定要初始化賦值,這不是運行時錯誤,這是個早期錯誤,編譯器在執行腳本以前會檢測早期錯誤。測試

咱們接着看下一個問題:

let a = 1;
let a = 2;

var能夠重複聲明變量,那麼let和const能夠嗎?答案是不能夠。你能夠認爲let和const聲明的變量名稱在該做用域內是惟一的,不能重複聲明。那若是用var能夠覆蓋let聲明的變量嗎?答案是不能。無論你是let或const先聲明變量var後面重複聲明,仍是var先聲明變量let或const後聲明都會報錯。這個錯誤是一個早期錯誤。

注意:let/const跨腳本聲明重複變量也會報錯。但這個時候的錯誤被認爲是運行時錯誤,不是早期錯誤。上面所指的let/const聲明都指在同一做用域下。

塊(Block)

上面列出了var、let、const靜態語義上的區別。在該小節中我會講述在javascript內部它們之間的不一樣,不過在此咱們先要了解(塊)Block,能夠說let、const是由於Block存在的。
不過提到Block以前咱們須要花幾分鐘瞭解幾個名詞:

我拿個例子簡單說明一下:

//全局聲明
var a=1;
let b=1;
const c=1;

function foo(){};
class Foo{};
{
   //塊級聲明
   var ba=1;
   let bb=1;
   const bc=1;

   class BFoo{};
   function bfoo(){}
}
  1. LexicallyDeclaredNames(詞法聲明名稱列表):« bb,bc,bfoo,BFoo »
  2. LexicallyScopedDeclarations(詞法做用域聲明列表):« let bb=1,const bc=1,function bfoo(){},class BFoo{} »
  3. VarDeclaredNames(var聲明名稱列表):« ba »
  4. VarScopedDeclarations(var做用域聲明列表):« ba=1 »
  5. TopLevelLexicallyDeclaredNames(頂級詞法聲明名稱列表):« b,c,Foo »
  6. TopLevelLexicallyScopedDeclarations(頂級詞法做用域聲明列表):« let b=1,const c=1,class Foo{} »
  7. TopLevelVarDeclaredNames(頂級var聲明名稱列表):« a,ba,bfoo »
  8. TopLevelVarScopedDeclarations(頂級var做用域聲明列表):« a=1,ba=1,function foo(){}»

注:« »結構是ECMAScript中的一個規範類型,表示一個List,具體你能夠認爲它是一個類數組(固然實際確定不是,只是方便理解)

有沒有看到怪異的地方?function聲明在頂級做用域(TopLevel)中被視爲var聲明,而不在頂級做用域也就是Block或catch塊中被認爲是詞法聲明,這就致使了一些有趣的事情。
Block只有前四個列表,函數(function)和腳本(script)只有後四個列表(其實函數和腳本也只有前四個,不過前四個列表的值取的是後四個列表的值)。Block雖然有本身的做用域可是它和函數有着本質上的區別。函數和腳本你能夠當作是相互獨立的而Block是屬於function和script的一部分。具體就是Block中的var聲明同時也被認爲是頂級聲明,無論你嵌了多少層塊在裏面都不會變,由於Block沒有頂級做用域。

理解了上面的8個名稱,咱們再來看看Block中的聲明與function和script中有何不一樣:

  1. LexicallyDeclaredNames中若是包含任何重複項,則語法錯誤。
  2. LexicallyDeclaredNames中出現的任何元素在VarDeclaredNames聲明中出現,語法錯誤。

規則1很正常,LexicallyDeclaredNames這個列表裏不能有重複項,即不能重複聲明。
規則2這就頗有意思了,咱們上面說到了在Block中function聲明屬於詞法聲明,因而你會在Block中看到:

{
  var foo=1;
  function foo(){}        
//Syntax Error,var和function不能聲明同一個標識符,腳本和函數中是不存在這個問題的。

//我大膽推測一下,可能在不久的未來腳本和函數中var和function也不能聲明同一個標識符了。
}

補充規則1中function聲明

{
  function a(){};  
  function a(){};      //it's ok,no syntax Error
}
//-----------------------
'use strict';
{
  function a(){};  
  function a(){};      //error, syntax Error redeclaration a; 
}

這裏我不得不吐槽一下了,就由於在非嚴格模式下Block中的function能夠重複聲明害我覺得規範1我理解錯了,致使我把文檔中有關Block規範說明部分翻來覆去看了好幾遍,最後我纔在規範文檔的附錄中找到緣由:爲了實現網頁瀏覽器的兼容性,容許在非嚴格模式下的Block中的function能夠重複聲明。

這裏有個建議,最好永遠不要在一個做用域內同時使用var和let/const聲明,還有不要在Block中使用var聲明,至於Block中的function聲明,除非你確切的知道你須要這個function作什麼,不然也不要在Block中使用function。Block中的function是如此的怪異。

1.非嚴格模式下,block中的function聲明的標識符會被提到頂級做用域下,可是隻提標識符,並賦值undefined,不提函數體。你能夠把它當作是一個var聲明的變量,具體以下:

console.log(foo);            //undefined
{
   function foo(){
      console.log(1);
   }
}
foo();                      //1

2.非嚴格模式下,block中的function聲明的函數對象對這個block來講造成了一個閉包,我認爲‘閉包’這個詞是最好的解釋:

var a = 'outer a';
{
   let a = 'inner a';
   function foo(){
      console.log(a);
   }
}
console.log(a)              //outer a
foo();                      //inner a,     not outer a

3.嚴格模式下,block中的function聲明只能在block中訪問到,離開這個block沒法訪問:

'use strict';
console.log(foo);            //Uncaught ReferenceError: foo is not defined
{
   function foo(){
      console.log(1);
   }
}
foo();                       //Uncaught ReferenceError: foo is not defined

出現這種狀況是由於ES5以前,block中不能出現function聲明,可是不一樣的瀏覽器實現不同,到了如今只能經過瀏覽器擴展進行填補。在非嚴格模式下,編譯器進行全局聲明實例化是也就是上篇文章中說道的GlobalDeclarationInstantiation方法時會對block、switch中case和default語句中的function聲明進行額外的操做,若是function聲明的標識符在全局環境下沒有找打其它的詞法聲明名稱即在TopLevelLexicallyDeclaredNames列表中不存在function聲明的標識符,則在全局環境記錄下建立function綁定,可是設置的值不是聲明的函數體而是是undefined。函數中有類似的操做。

block中的一些注意點以及和function還有script中的區別我大體講了一下。那麼block是如何作到有塊級做用域的功能的呢?
我在上一篇文章中講到了執行上下文,提到執行上下文是編譯器用來跟蹤代碼執行時評估的一種規範設備,每一個執行上下文都有本身的LexicalEnvironment和VariableEnvironment組件。編譯器在評估Block作了以下操做:

  1. 讓oldEnv成爲正在運行的執行上下文(running execution context)的LexicalEnvironment。
  2. 讓blockEnv成爲一個新的聲明性環境,它的外部詞法環境引用指向oldEnv。
  3. 對block中的聲明進行實例化。
  4. 把正在運行的執行上下文(running execution context)的LexicalEnvironment設爲blockEnv。
  5. 讓blockValue成爲執行block中的代碼的結果。
  6. 把正在運行的執行上下文(running execution context)的LexicalEnvironment設爲oldEnv。
  7. 返回blockValue。

咱們看到了執行block中代碼時不會新建執行上下文,它只是改變了正在運行的執行上下文的LexicalEnvironment組件值,block運行完成後又恢復成之前的LexicalEnvironment組件,這指明瞭block中聲明的變量只在該block中起做用,這也表示爲何block是塊級做用域。這跟函數不同,執行函數時會建立新的執行上下文。
我這再說明一下,步驟3中的聲明進行實例化指得是LexicallyScopedDeclarations列表中的聲明,block不會對其中的var聲明進行操做。步驟5中的blockValue指得是block中最後一個語句執行後的返回值。

知道了這個,咱們來看個let和var在Block中的不一樣:

for(var i = 0;i < 10;i++){
   setTimeout(function(){console.log(i)})
}
//輸出10個10

for(let i=0;i<10;i++){
   setTimeout(function(){console.log(i)})
}
//輸出0到9

我這邊作個簡單說明:

  1. 把全局環境記錄記gec,for循環裏的環境記錄記爲bec,匿名函數的環境記錄記爲fec。
  2. gec的外部環境null,bec的外部環境gec,fec的外部環境bec。
  3. 第一個for循環中函數輸出i,fec中沒有i的記錄,向外找bec,沒有i的記錄,向外找找gec,發現i,值爲10,因此輸出10個10。
  4. 第二個for循環中函數輸出i,fec中沒有i的記錄,向外找bec,找到i的記錄,並輸出i,這個i是當前bec記錄中i的值,每次循環都會建立一個新的bec記錄。

變量提高(Hoisting)

咱們都知道var和function聲明在做用域內存在着變量提高,可是let/const或者class呢?究竟有沒有存在變量提高。這個問題存在着爭議,可謂仁者見仁智者見智。

我在上篇文章中提到了全局聲明實例化和block中的block聲明實例化以及沒有提到的function聲明實例化,你會發現一個關鍵,就是這些操做都是在執行代碼以前作的,全局聲明實例化在腳本執行以前進行,block聲明實例化在block中的代碼執行以前進行,包括函數也是如此。那麼聲明實例化到底是作什麼的呢?

具體的操做就是把存在LexicallyScopedDeclarations、VarScopedDeclarations、TopLevelLexicallyScopedDeclarations和TopLevelVarScopedDeclarations的信息進行操做,存到環境記錄中。這些詞都是靜態語義,也就在在腳本執行以前就已經存儲了。

var a = 1;
let b = 1;
//執行代碼前環境記錄(Environment Record)綁定了a,b,並給a賦值爲undefined,b不賦值。
//注:let、const和class只綁定(實例化)不初始化,var和function會進行初始化,function初始化指的就是整個函數。

//執行代碼時----------------
console.log(a);      //undefined   環境記錄中有a的這個綁定,而且值是undefined,因此輸出undefined
var a = 1;

//----------------
console.log(a);      //Uncaught ReferenceError: a is not defined   環境記錄中有a的這個綁定,可是沒有值,因此error。
//可能a is not defined改成a is not initialized更能讓人容易理解。
// not defined容易和undefined混淆。
let a = 1;

//一個更好的例子
var a = 1;
{
    console.log(a);        //Uncaught ReferenceError: a is not defined,not value 1;
    let a = 2;             //let聲明的變量實際上也提高了
}

正是這樣緣由致使「變量提高」存在爭議,一部分人認爲let、const、class和var同樣,在一開始就已經提高了,因此let、const、class存在「變量提高」。有的人認爲所謂「變量提高」,是指代碼不報錯,還能運行,而let、const、class會出現錯誤,因此不能算「變量提高」。

ECMAScript規範一直沒有給出準確的說明,甚至不一樣版本說法不同,在最新的ES8規範中雖然沒有給出準確的說明,可是規範定義了一個HoistableDeclaration文法,該文法中包含了FunctionDeclaration、GeneratorDeclaration和AsyncFunctionDeclaration文法。HoistableDeclaration文法又與ClassDeclaration和LexicalDeclaration(let/const的語法規則)文法組成Declaration文法。

這裏是否是能夠推斷出ECMAScript規範認爲let、const和class不存在「變量提高」呢。固然這只是個人一個推測。

結束語

到這裏let/const和var的解釋基本就完結了。我大體的對let/const以及var作了一個區別介紹,可是還有不少小的細節不能涵蓋到,若是感興趣想了解更多的話能夠查看官方文檔13.2 Block13.3 let/const和var。算上最開始的javascript強制轉化,這是我對ES8文檔講解的第三篇文章,以後我會陸續發表一些我對ES8文檔的理解,但願能與人一塊兒交流共進。

相關文章
相關標籤/搜索