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文档的理解,但愿能与人一块儿交流共进。