写点什么

你可能不是真的懂 let 和 const

用户头像
前端树洞
关注
发布于: 2021 年 04 月 09 日
你可能不是真的懂let和const

在 ES5 我们习惯使用 var,而 var 常常会给我们带来一些困扰,比如存在变量提升、允许重复命名(且后执行的会覆盖前执行的)、没有暂时性死区、没有块级作用域 。而 ES6 新增的let命令const命令,解决了这些困扰。

let

下面通过代码的方式来解答,变量提升、重复命名、暂时性死区、块级作用域,这些问题的解决和功能添加让平时开发的我们得到什么帮助。


  • 不存在变量提升

console.log(a); // undefinedvar a = 1;
// 实际运行是这样的var a;console.log(a); // undefineda = 1;
复制代码

这样的代码,应该都见过吧?这就是变量提升。因为这种奇怪的行为其实代码是很难维护的。所以let解决了这样的问题。


console.log(a);let a = 1; // a is not defined
复制代码

let不存在var那样的情况。当然要是直接使用let定义变量不赋值,使用的时候值当然也是undefined


  • 不允许重复声明

使用var重复声明变量应该都是家常便饭了,但是let不允许在相同作用域内,重复声明同一个变量。

{  var num = 123;  console.log(num); // 123   var num = 456;  console.log(num); // 456}
{ let num = 123; console.log(num); // 123 let num = 456; // SyntaxError: Identifier 'num' has already been declared}
复制代码

输出的结果大家应该都能明白,但是为什么上面加了大括号呢?

因为v8引擎做了一些特殊的处理 这里let,所以let在控制台中不加上大括号可以重复声明。


  • 暂时性死区

只要块级作用域存在let命令,他所声明的变量就“绑定“这个区域,不再受外部的影响。

var num = 123;
{ num = 456; // ReferenceError: Cannot access 'b' before initialization let num;}
复制代码

什么意思呢?就是使用let命令声明变量之前,该变量不可用。


还有一些比较隐蔽的错误,但是在开发中经常会遇到

function test(x = y, y = 2){  return (x, y);}
test(); // ReferenceError: Cannot access 'b' before initialization
复制代码

这也是暂时性死区的好处,避免开发时的坏习惯,一定在变量声明后才能使用,否则报错。


  • 块级作用域

内层变量可能会覆盖外层变量。

var num = 123;function fun(){  console.log(num);  if(false){    var num = 456;    console.log(num);  }}
fun(); // undefined

// 代码实际运行funcion fun(){ var num; console.log(num); if(false) { num = 456; console.log(num); }}var num;num = 123;
fun(); // undefined
复制代码

这段代码只是想在函数中打印外层的num,在if判断内打印出自己定义的num,可是打印出来的却是undefined,这是因为变量提升导致的,同时也是因为没有块级作用域。


没有块级作用域还会导致循环变量泄露为全局变量。

for(var i = 0; i < 5; i++){  console.log(i); // 0 1 2 3 4 }console.log(i); // 5
复制代码


块级作用域不会影响全局作用域的好处。

{  var a = 1;  let b = 2;}console.log(a); // 1console.log(b); // ReferenceError: b is not definedat <anonymous>:6:13
复制代码

为什么a能正常打印b却会报错?因为用var定义的变量不存在块级作用域也就是全局都能访问,只有在函数中使用var才存在作用域不影响全局,因此 ES5 中常常会听到函数作用域,但是使用let完全不需要担心,只要是大括号包住那么JavaScript就认为他存在块级作用域因此可以避免影响全局作用域。


var a = [];for (var i = 0; i < 10; i++) {  a[i] = function () {    console.log(i);  };}a[6](); // 10
复制代码

为什么a[6]()会打印出10呢?

因为i是用var声明的,上面也说了,用var声明的变量是全局的,所以全局都能访问,每一次循环,变量i的值都在改变,且循环体内给数组赋值的函数内的i是全局的,所以输出的值是10。可能有的人还没明白或者会说这个应用场景是什么。那再来看看下面的例子

// var定义ifor (var i = 1; i < 5; i++) {    setTimeout(() => console.log(i), 1000)  // 5 5 5 5}
// let定义ifor (let i = 1; i < 5; i++) { setTimeout(() => console.log(i), 1000) // 1 2 3 4}
复制代码

这样有应用场景了吧。我希望控制他过多久输出,可是为什么是打印4个5呢?这里涉及的知识点比较多(闭包,提升,事件循环),今天讲的是let就不要偏题了,后面我会单独讲解,拿这个出来讲是因为let恰恰能解决这个问题。

因为用let声明的变量,不是全局范围有效的,因此当前的i只在本轮循环中有效,所以每一次循环其实都是一个新值,所以他们互不干扰。可能还有人会问每次都是新值,那怎么知道当前循环的值呢?这就是JavaScript引擎内部做的事情了,现在只要知道有这件事即可,后面会单独讲解,毕竟涉及的知识点是比较多的,不要跑题了。


  • 块级作用域声明函数

function fun(){ console.log('outside') }
(function (){ if(false) { function fun(){ console.log('inside') } } fun();}());
// ES5的实际运行(funciton (){ function fun(){ console.log('inside') } if(false){} fun(); // inside}());
复制代码

先说结果,ES5中当然打印出来的是insideES6中会提示fun is not a function 。应该都没问题。

但是这实际说明了什么?应该有些人没明白,那我来解释一下。先看看这个

if(true){  function fun(){}}
复制代码

ES5中规定了,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域中声明。

后来ES6 引入了块级作用域,也明确了允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。可是需要兼容旧代码,所以附录B 里面也说明了浏览器可以不遵循上面的规则,有自己的行为方式。也就是

  • 允许在块级作用域内声明函数

  • 但是运行时会类似于var,也就是会提升到全局作用域或函数作用域的头部。

  • 同时,函数声明还会提升到所在的块级作用域的头部。

需要注意:上面的规则只在ES6环境中实现有效,其他环境不需要遵守。


根据上面的规则再来看看ES6实际运行的效果

function fun() { console.log('outside') }(function () {  var fun = undefined;  // 类似于var 这是函数作用域  if(false) {    let fun1 = function () { console.log('inside') } // 块作用域    fun = fun1;  }  fun(); // fun is not a function}())
复制代码

也就是说,在块级作用域中允许声明函数,但是又因为块级作用域的原因,且要兼容旧代码,所以块级作用域中的fun,没有被提升到外部,而提升到块级作用域的头部,这样应该都明白了吧。

const

const 是什么?const 声明的是一个常量,声明时必须同时赋值,且值不可变。

// const声明的常量值不可变const P = 3.14159;P = 3; // TypeError: Assignment to constant variable.
// const声明常量时须赋值const num; // SyntaxError: Missing initializer in const declaration
复制代码


constlet 一样存在块级作用域、暂时性死区

// 暂时性死区console.log(P); // ReferenceError: Cannot access 'P' before initializationconst P = 3.14159;
// 块级作用域{ const P = 3.14;}console.log(P); // ReferenceError: P is not defined
复制代码


其实const本质是保证变量所指向的内存地址不变,也就是说,引用类型中的属性值是可变,可添加的。

const obj = {};obj.name = '公众号:前端树洞';console.log(obj); // { name:'公众号:前端树洞' }
obj.name = '欢迎关注公众号:前端树洞';console.log(obj); // { name:'欢迎关注公众号:前端树洞' }
const arr = [1, 2, 3];arr.splice(1);console.log(arr); // [1]
arr.push(4);console.log(arr); // [1, 4]
复制代码


  • 全局(window)

最后再来看看上面经常提起的全局到底说了什么。

全局作用域也叫做顶层对象,在浏览器中是window对象,在nodeJS中是global对象。前面经常会提到var定义的变量是全局可以访问的。先看代码

var num = 123;console.log(window.num); // 123
function fn(){ console.log(123123); }window.fn(); // 123123
let num2 = 456;console.log(window.num2); // undefined
const num3 = 789;console.log(window.num3); // undefined
class fn{}window.fn; // undefined
复制代码

顶层对象与全局属性挂钩了。什么意思呢?

就是使用var命令和function命令声明的都是顶层对象的属性。所以能通过window找到对应的属性。ES6改变了这一点,但又为了保持兼容性,因此现在varfunction声明的变量和函数依然属于顶层对象的属性,但是letconstclass声明的全局变量,不属于顶层对象的属性。


发布于: 2021 年 04 月 09 日阅读数: 118
用户头像

前端树洞

关注

还未添加个人签名 2020.07.16 加入

不仅能倾听,还能分解,前端,面试,nodeJS,源码都有,公众号同名(前端树洞)

评论

发布
暂无评论
你可能不是真的懂let和const