我在上一篇文章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(){}
}
  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个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,所以输出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文档的理解,希望能与人一起交流共进。

javascript中var、let、const声明的区别相关推荐

  1. var,let,const 声明中一般人不知道的几个点

    关于var,let,const 声明变量时,有几个特别注意的点,面试的时候极容易被问到,但是很多人特别容易说不清.let的作用域呀,暂时性死区,const作用域等. 文章目录 前言 一.小姐姐知道的l ...

  2. java引用类型和值类型_[Java教程]JavaScript中值类型和引用类型的区别

    [Java教程]JavaScript中值类型和引用类型的区别 0 2017-02-24 00:00:35 JavaScript的数据类型分为两类:原始类型和对象类型.其中,原始类型包括:数字.字符串和 ...

  3. ES6中使用let, const声明的变量, 在window对象下是获取不到的

    ES6中使用let, const声明的变量, 在window对象下是获取不到的 今天在写demo的时候发现, 使用let或者const声明的变量, 在window对象下使用this来获取发现获取不到 ...

  4. JavaScript中var、let和const的区别

    一.前言 在ES6(ES2015)出现之前,JavaScript中声明变量就只有通过 var 关键字,函数声明是通过 function 关键字,而在ES6之后,声明的方式有 var . let . c ...

  5. JavaScript 中 var 、let、const区别

    问题 var . let. const 都可以用来声明变量,那么它们的区别是什么? 思考思路 变量的数据类型(数字,字符串,数组 and so on) 变量的作用域 ( 全局 or 局部 or 某个代 ...

  6. var let const声明变量的区别

    在js中定义变量的方式有三种,其中let和const关键字是来自ES6中的,下面将逐一介绍各个关键字声明变量的特点. var声明变量 var 是一个 JS关键字,用来声明变量( variable 变量 ...

  7. Js中var,let,const的区别

    一:区别: 1.var声明的变量属于函数作用域,而let和const声明的变量属于块级作用域:(js作用域在上篇文章) 2.var声明的变量存在变量提升,而let和const没有 3.var声明的变量 ...

  8. es6中变量/常量的声明以及区别

    var / let /const var声明变量 var存在全局作用域和函数作用域两种,并且有变量提升 let声明变量 const声明常量 let和const对比var声明有以下特性/区别 变量声明不 ...

  9. Javascript中的循环变量声明,到底应该放在哪儿?

    不放走任何一个细节.相信很多Javascript开发者都在声明循环变量时犹 豫过var i到底应该放在哪里:放在不同的位置会对程序的运行产生怎样的影响?哪一种方式符合Javascript的语言规范?哪 ...

最新文章

  1. 摊手:工作五年我拿了两次 N+1 赔偿!
  2. 使用代码配置 NHibernate
  3. qs.parse()、qs.stringify()使用方法
  4. Microsoft Azure_Fabric
  5. Google 翻译的妙用
  6. pdo mysql分页_php运用PDO连接数据库,实现分页效果
  7. 使用SDK进行二次开发流程简述
  8. 百度云服务器bcc搭建php环境,使用百度云服务器BCC经验谈
  9. vscode自动整理代码_再见了, VS Code!
  10. 2018山西中考计算机软件名称,2018年山西省中考试题
  11. 计算机网络的概述以及网络的组成
  12. amlogic logo amlogic rk tvbox 电视盒 定制化开发(一) 开机logo 定制
  13. 社保证照片怎么做?一招教你get既专业又好看的证件照!
  14. 喜玛拉雅——徐薇翻唱合集
  15. 学数学计算机考研,计算机考研考数学
  16. 服务器迁移到阿里云的解决方案
  17. IDEA中创建编写JSP
  18. 基于单片机的空气检测系统的设计
  19. 【区块链羊毛 一分钟 20+】IOST社区共建计划,参与得IOST奖励
  20. UML画图之构件图和部署图

热门文章

  1. Android Mvp 架构
  2. Win7 安装资料及教程
  3. 周志华讲座---关于人工智能---科普性质---天地工学讲坛2017.11.30
  4. mysql 回表 覆盖索引_MySQL 的覆盖索引与回表的使用方法
  5. mysql密码高级_MySQL数据库高级操作(图文详解)
  6. android device monitor命令行窗口在哪里_Vulkan在Android使用Compute shader
  7. c# 字典按ascii 排序_sort命令-将文件进行排序并输出
  8. Java设计模式(十六):生成器设计模式
  9. python exe运行报 编码错误_python运行显示编码错误
  10. log4j的使用 与 父接口 slf4j 门面模式(外观模式)