博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
javascript中var、let、const声明的区别
阅读量:5990 次
发布时间:2019-06-20

本文共 7688 字,大约阅读时间需要 25 分钟。

我在上一篇文章中的最后稍微提到了有关var、let、const声明的区别,在本篇中我会重点来分析它们之间到底有什么不同。

提到var、let、const中的区别很多人一下子就想到了,var声明的变量是全局或者整个函数块的而let、const声明的变量是块级的变量。var声明的变量存在变量提升,let、const声明的变量不存在变量提升。let声明的变量允许重新赋值,const声明的变量不允许重新赋值。那么它们之间真的只有这么一点区别吗,我们先来看下面一个例子:

注:本篇文章中的所有例子都以最新版chrome浏览器为标准(低版本浏览器实现会有区别)。

//我们看一下这三句话,你认为会发生什么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则报错。

该代码是运行在非严格模式下的,严格模式则报错,值得注意的是严格模式下上面三句话都是因为标识符let是保留字而报错的。有兴趣可以在严格模式和非严格模式下测试let let = 1;报错原因是不同的。

下面的所有代码都在非严格模式下进行,如果是严格模式我会明确指出。

那么上面三句话中的标识符let改为const会怎么样?无论是严格模式还是非严格模式都报错,错误原因是因为const是关键字,这时候问题又来了,为什么标识符let和const的行为会不同呢?这个锅说到底还是得ES5规范背,在ES5规范中const被认为是未来保留字(FutureReservedWords)而let只有在严格模式下才被认为是未来保留字,这导致var可以声明let却不能声明const,那到了ES6时代为什么不改呢?哎!不是不改而是心有力而余不足啊,鬼知道在ES6时代之前有多少代码中出现过var let这个声明啊,这要是改了得有多少网站得炸啊。

基于上面的原因,你看到下面的代码时不要惊讶:

var let = 1;console.log(let);                  //1let 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 afoo();                      //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个10for(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,所以输出undefinedvar 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做了一个区别介绍,但是还有很多小的细节不能涵盖到,如果感兴趣想了解更多的话可以查看官方文档和。

算上最开始的javascript强制转化,这是我对ES8文档讲解的第三篇文章,之后我会陆续发表一些我对ES8文档的理解,希望能与人一起交流共进。

转载地址:http://bvnlx.baihongyu.com/

你可能感兴趣的文章
在indesign中如何分栏
查看>>
如何在Evolution中加密(五)
查看>>
java中的参数传递
查看>>
iOS之Storyboard导航大揭秘(1)
查看>>
家用PC机打造VSphere5.1 测试环境: 之测试虚拟机
查看>>
远程调试 Azure Web App
查看>>
Vmware vSphere 5.0系列教程之三 vCenter介绍及安装配置
查看>>
OpenExpressApp make business engineers develop applications
查看>>
《WCF技术内幕》翻译18:第1部分_第4章_WCF101:从外部剖析WCF
查看>>
《机器学习实战》k最近邻算法(K-Nearest Neighbor,Python实现)
查看>>
中小企业管理解决方案
查看>>
一步一步学Silverlight 2系列(9):使用控件模板
查看>>
企业架构 - 组织角色和技能
查看>>
Exchange Server2010系列之十二:部署及配置邮箱高可用DAG
查看>>
mssql 字增自段怎样重置(重新自增)|清空表已有数据
查看>>
Trojan.DL.Win32.Hmir.hl的清除方法 采用驱动提供服务的木马病毒
查看>>
让UITableView的Cell都变成静态的
查看>>
win8:添加WinJS控件
查看>>
[转] c++ try catch 问题
查看>>
如何解决关于TableView里面cell随机显示的问题
查看>>