资讯专栏INFORMATION COLUMN

javascript中var、let、const声明的区别

Yujiaao / 1137人阅读

摘要:声明的变量存在变量提升,声明的变量不存在变量提升。声明的变量允许重新赋值,声明的变量不允许重新赋值。注意跨脚本声明重复变量也会报错。中出现的任何元素在声明中出现,语法错误。中的是如此的怪异。对中的声明进行实例化。

我在上一篇文章javascript中词法环境、领域、执行上下文以及作业详解中的最后稍微提到了有关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);                  //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(){}
}

LexicallyDeclaredNames(词法声明名称列表):« bb,bc,bfoo,BFoo »

LexicallyScopedDeclarations(词法作用域声明列表):« let bb=1,const bc=1,function bfoo(){},class BFoo{} »

VarDeclaredNames(var声明名称列表):« ba »

VarScopedDeclarations(var作用域声明列表):« ba=1 »

TopLevelLexicallyDeclaredNames(顶级词法声明名称列表):« b,c,Foo »

TopLevelLexicallyScopedDeclarations(顶级词法作用域声明列表):« let b=1,const c=1,class Foo{} »

TopLevelVarDeclaredNames(顶级var声明名称列表):« a,ba,bfoo »

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中有何不同:

LexicallyDeclaredNames中如果包含任何重复项,则语法错误。

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做了如下操作:

让oldEnv成为正在运行的执行上下文(running execution context)的LexicalEnvironment。

让blockEnv成为一个新的声明性环境,它的外部词法环境引用指向oldEnv。

对block中的声明进行实例化。

把正在运行的执行上下文(running execution context)的LexicalEnvironment设为blockEnv。

让blockValue成为执行block中的代码的结果。

把正在运行的执行上下文(running execution context)的LexicalEnvironment设为oldEnv。

返回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

我这边做个简单说明:

把全局环境记录记gec,for循环里的环境记录记为bec,匿名函数的环境记录记为fec。

gec的外部环境null,bec的外部环境gec,fec的外部环境bec。

第一个for循环中函数输出i,fec中没有i的记录,向外找bec,没有i的记录,向外找找gec,发现i,值为10,所以输出10个10。

第二个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 Block和13.3 let/const和var。
算上最开始的javascript强制转化,这是我对ES8文档讲解的第三篇文章,之后我会陆续发表一些我对ES8文档的理解,希望能与人一起交流共进。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90105.html

相关文章

  • 总结下varletconst 区别

    摘要:前言和的区别是老生常谈,看到网上一些文章的总结,有的不太全面,甚至有的描述不太准确,在这里尽量全面的总结下这三者的区别。最后以上大概是总结后的内容,看来,还是多用吧。 前言 var 和 let 的区别是老生常谈,看到网上一些文章的总结,有的不太全面,甚至有的描述不太准确,在这里尽量全面的总结下这三者的区别。 let 是 ES6新增的变量类型,用来代替 var 的一些缺陷,跟 var...

    pingink 评论0 收藏0
  • ES2015系列--块级作用域

    摘要:在的闭包中,闭包函数能够访问到包庇函数中的变量,这些闭包函数能够访问到的变量也因此被称为自由变量。在之前最常见的两种作用域,全局作用局和函数作用域局部作用域。 关于文章讨论请访问:https://github.com/Jocs/jocs.... 当Brendan Eich在1995年设计JavaScript第一个版本的时候,考虑的不是很周到,以至于最初版本的JavaScript有很多不...

    darkbug 评论0 收藏0
  • 深入理解letvar区别(暂时性死区)!!!

    摘要:会出现这样的情况是因为拥有暂时性死区。规定暂时性死区和语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。 首先我们应该知道js引擎在读取js代码时会进行两个步骤: 第一个步骤是解释。 第二个步骤是执行。 所谓解释就是会先通篇扫描所有的Js代码,然后把所有声明提升到顶端,第二步是执行,执行就是操作一类的。 我们先来看个简单的变量提升...

    tanglijun 评论0 收藏0
  • javascript 声明变量varletconst详解及示例

    摘要:概述发布前,只能通过声明变量的方式,常量块级变量函数变量这些概念的差别都不能很好的体现出来,于此同时,加入你要使用或者提供一个,声明的变量可随时被修改和重新分配的问题,会让你时刻担心代码是否能正常运行。 1. var、let、const概述 ES6发布前,Javascript只能通过var声明变量的方式,常量、块级变量、函数变量这些概念的差别都不能很好的体现出来,于此同时,加入你要使用...

    tuomao 评论0 收藏0
  • javascript变量声明

    摘要:变量提升是在预编译的过程中发生的,赋值为被声明的变量还是在原来的地方,真正被赋值块级声明块级声明用于声明在指定块的作用域之外无法访问的变量。只有执行变量声明语句后,变量才会从中移出,然后才可以正常访问。 在代码中,声明变量是基础,但是在javascript中,经历了从var到let,const的变化,到底有什么本质上的区别呢? 本文的原文在我的博客中:https://github.co...

    zhongmeizhi 评论0 收藏0

发表评论

0条评论

Yujiaao

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<