『ES6篇』之你所不知道的var、let与const

文章介绍

 本文将围绕着ES6新增的变量声明关键字let、const展开,从预编译过渡到作用域链再到var声明与let声明之间的区别,以经典的var循环面试题解答引出let循环,并讲解为何let循环能够解决var循环的问题,最后介绍ES6新增常量声明关键字const以及对象冻结Object.freeze。

知识点

  1. 预编译过程
  2. var声明提升
  3. let声明的特点
  4. var与let声明的区别
  5. var定义循环的经典面试题
  6. 如何解决var循环产生的问题
  7. 讲解let循环为什么不会出现问题
  8. const常量定义
  9. 对象冻结Object.freeze

一、预编译

&esmp;想必大家学习JavaScript的时候一定听过一个词声明提升,那么什么是声明提升呢?注意:在ES5当中能够进行声明提升的只有var变量声明以及函数声明定义变量及函数时才会发生声明提升。
声明提升

图1:声明提升

 从图一中我们可以到一个现象:变量在未声明前被打印输出函数在未被定义前成功执行。想必大家都知道JS引擎执行JS代码的方式是解析一行执行一行的,那么为什么会出现上述的现象呢?按道理应该在定义完之后才能被正常使用的变量、函数是如何做到提前执行的呢?这就跟这一小节的预编译有关系了。

1、什么是预编译

 JavaScript解析首先会检查代码中是否存在语法错误,其次便是执行预编译过程,最后才是我们所认知的解释一行代码,执行一行代码的过程。那么这个预编译做了什么呢?从上面的现象我们也能猜到,预编译的过程中将函数、变量的声明提升到了当前作用域(var不受块级作用域影响)的最顶层。
预编译模拟过程

图2:预编译模拟过程

2、预编译的过程

 预编译的过程分为全局执行时的预编译以及函数执行时的预编译,这时就会从产生两个不同的对象,分别是GO(Global Object)、AO(actiavtion object) 也就是我们熟知的全局对象以及函数上下文。
预编译过程

图3:预编译过程

 就以图1的代码举个例子:
预编译举例


图4:预编译案例


 当预编译完成之后我们将得到如上的一个GO对象,此时执行打印和函数执行代码时因为全局作用域中已经定义了variable和fn函数,因此得以正常执行。
 以上便是对预编译的一个简单介绍,有机会笔者会就预编译以及作用域、作用域链相关知识点进行一个串联,到时再着重介绍,本文可以当做预编译、作用域相关知识的前瞻。

二、var声明与let声明

 两者最大的区别听的最多的肯定是var声明会提升而let声明不会提升。不知道大家有没有听说过let声明的其他特点,我们先总结一下let声明的三大特点:

  1. let 声明在函数、全局作用域下都无法重复声明
  2. let声明会产生一个独立块级作用域且只作用于块级作用域
  3. let声明将产生暂时性死区

1、暂时性死区

 什么是暂时性死区呢?指的是在定义JS引擎解析到let声明时,将这个变量设置为不可使用,如果在代码执行到let声明之前使用了这个函数会抛出引用类型错误,同时注意到抛出的错误不是variable is not defined,这也就是let声明不提示的原因,需要执行了声明那一行代码之后才可以使用let声明的变量。
暂时性死区

图5:暂时性死区

2. 创建一个块级作用域

 首先我们来说说let会创建一个独立块级作用域,这一点非常容易验证。在ES5中我们在全局对象上使用var a = 10声明一个变量与直接使用a=10并没有区别,为什么这么说呢?因为这两种声明(赋值方式)都是将变量a挂在到全局对象window上,这实际上是不合理的,因此ES6中的变量声明let解决了这个问题,会将声明的变量保存在一个独立作用域中,让我来尝试一下:
独立作用域

图6:独立作用域

 同时,与var声明还有一个非常重要的不同点:var声明是受限于函数作用域的,而let声明受限于块级作用域。什么意思呢?**在块级作用域中同时使用var与let声明,在块级作用域外的作用域中无法访问到let声明的变量而可以访问到var声明的变量。**下面举例说明:
let生效范围

图7:let生效范围

 从上图其实可以看到三个知识点:1.let在块级作用域中定义(ps:{}是一个块级作用域)、2. 函数声明以及var变量声明不受块级作用域限制,只函数作用域限制、3. 函数声明提升的范围是在块级作用域内但执行完毕之后是赋值给外界作用域的,及在外界可以访问到函数变量。因此不推荐在块级作用域中使用函数声明的方式定义函数,而应该使用函数字面量的形式。

3. 不可重复定义

 在ES5中使用var声明变量是允许重复声明的,通过第一小节预编译的知识大家应该也可以明白原因:在预编译的时候JS引擎就收集了所有的var声明的变量并将声明提升至作用域的顶端。
 作为ES6新的定义关键字,let不允许相同的变量被重复的定义。
重复定义

图7:重复定义

三、var经典循环面试题

 大家是否在笔面试中的循环题或事件委托题中被问到这么一道循环的问题:

1. 题一:请问以下代码的执行结果是什么

var循环题-1

图8:var循环题-1

 如果你脱口而出0-9,那么你对于var的理解以及作用域的理解可以说相当差了。那我们来解读一下这一段代码:

for循环的两个块级作用域

 首先我们要知道for循环其实是有两个块级作用域的,第一个块级作用域是判断区域的小括号()部分,第二个块级作用域是循环执行大括号{}部分,同时,小括号部分是大括号块级作用域的父级作用域。但在ES5中我们使用的函数定义以及var声明是不受块级作用域影响的,也就是for循环可以被改写为:
for循环改写

图9:for循环改写

 从上述的写法中我们直接就可以看出,每一次的i++实际上是对最外层作用域中的变量i进行修改(修改的都是相同的一个变量),同时在给数组arr赋值一个函数字面量时,产生了闭包,匿名函数的作用域链中保存了外层作用域的引用。当这个函数被执行时,会根据作用域去寻找变量i,但是在执行之前,经过10次循环,这时外层作用域的i已经是等于10了,这个时候无论是数组中的哪一个匿名函数执行,都是拿到相同外界作用域中的相同变量i=10这个值,因此本题的结果是输出10个10

2. 题二:请问以下代码执行结果是什么

var循环问题2

图10:var循环问题-2

 别着急,如果这时你脱口而出10个10,那么你又错了。这题与上一题类似,而又不完全相同,什么意思呢?经过上一题你知道了for循环的改写,那么此时你将两个for循环都以改写带入,你会发现第一个for循环与上题是完全一致的,形成了10个闭包匿名函数。但当你带入第二个循环时你会发现,又遇到了var i = 0,这时你应该明白了,第二次for循环每一次执行时又改变了外部作用域的i,因此每次执行数组匿名函数时,外界作用域中的i的值就是当前下标,因此本题的答案是:0-9。这也是解决var循环的一个方案,再使用一次相同变量的循环,babel转码也是这么做的,给大家看看babel是如何将let循环转换为var循环:
babel转码

图11: babel转码

3. 题三:请问下列代码执行结果是什么?

let循环

图12: let循环

 通过之前我们对for循环的改写加上let定义的知识,大家可以把for循环具有两个作用域且为父子关系以及let声明不会提升以及作用域块级作用域内两个知识点,可以得出:let不会像var一样将变量i定义在全局作用域上,而是定义在括号所在的父级作用域中,同时for循环每一次都会创建不同的父子级作用域,每一次的结果类似:
let循环结构

图13: let循环结构

 那么这一题的答案也显而易见了。这也是在ES6之后推荐使用let循环的原因,可以让异步执行的方法、指向一个正确的i值。

4、ES5解决var循环问题

 在ES5是如何解决var循环的问题呢?我们使用到的是立即执行函数创建一个作用域以保存每次执行的下标i,内部的闭包函数保存这个作用域,按照作用域链查找的规则,优先查找立即执行函数作用域上的变量i
立即执行函数解决var循环问题

图14: 立即执行函数解决var循环问题

四、const定义常量

 const关键字声明的变量包括let关键字声明的特征,同时const所定义的原始值无法被修改。看到我说原始值大家一定能那const所定义的引用值呢?const定义的引用值指向栈空间的地址无法被修改,什么意思呢?就是说const a = {},我们无法直接使用a = {}的方式给变量a赋值一个新的对象,但是我们对变量a指向的对象的属性进行变更是被允许的。
const定义对象

图15: const定义对象

 const与let一致的特性就不再举例,大家可以自行测试。const一般是用于定义不被修改的常量以及地址不发生修改的引用值,比如引入模块一般使用const xxx = require('xxx')的方式。当然,我们也是可以解决对象属性会被修改的问题。

五、解决const引用值属性修改问题

 使用Object.freeze方法将对象冻结,freeze方法是修改了对象所有属性的增删改属性描述符,这样对象的属性就无法被删除、添加以及修改。当然我们需要考虑到如果对象中嵌套了一个对象属性时,freeze方法无法深层次的冻结,因此我们需要自己定义一个递归函数,逐层冻结对象。

1. Object.freeze基本使用

freeze基本使用

图16: freeze基本使用

2. 封装freeze递归方法

递归freeze

图17: 递归freeze

3. 重写Object.freeze

 刚才提到Object.freeze是通过修改所有属性的增删改属性描述法来达成冻结对象的方法,那么我们是否可以使用Object.defineProperty实现类似的效果呢?当然可以,下面就以Object.defineProperty重写一个的Object.freeze
definProperty实现freeze

图18: definProperty实现freeze

 当然我们也可以使用Object.defineProperties实现,这里我就不再写了,原理是一样的。

六、总结

 首先,得跟阅读本文章的读者说一句抱歉。笔者编写文章的念头都是想到什么写什么,因此并没有特别连贯的一个知识点连接,比方说本文用到了特别多的作用域、作用域链相关的知识,因此按道理来说应该先分享作用域相关的文章。但是笔者编写文章更大的一个念头是检验自己掌握知识的程度,因此只能说声抱歉。如果大家在作用域方面存在知识盲区,可以先观看相关优秀文章后再回过头来阅读本文,当然也有可能这个时候笔者已经更新了作用域相关的文章~
 我是Donp1,我们下次见~🐻‍❄️
 代码仓库:https://gitee.com/Crivk/csdn-related-demo/tree/master/ES6/let、const

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Donp1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值