从作用域的冰山一角看let和var的异同

从作用域的冰山一角看let和var的异同

情景引入

DOM部分

  <!--button{按钮$}*5-->
  <button>按钮1</button>
  <button>按钮2</button>
  <button>按钮3</button>
  <button>按钮4</button>
  <button>按钮5</button>

JS部分

  var btns = document.querySelectorAll("button")
  for (var i = 0; i < btns.length; i++){
    btns[i].addEventListener('click', function () {
      console.log('第' + (i + 1) + '个按钮被点击')
    })
  }

这应该算一个很老生常谈的话题了,大家一看也都知道这是牵扯到作用域的话题,我们也用它来引出今天想和大家聊聊的。
result1
结果也很明显,无论点击哪一个按钮都是在控制台输出第六个按钮被点击。这是因为在es5中,if和for的大括号,也就是块,都没有块级作用域的概念,在这里定义的i本质上是全局变量,等我们点击触发控制台输出事件的时候,i已经是跳出for循环的btns.length值,也就是5。
这样说可能还是不够清晰,所以我有了一个探究其过程的思路:
我认为每一次for循环可以宽泛的理解为:
当var i = 0时,我们聚焦大括号里的内容:

	{
	var i = 0
	btns[i].addEventListener('click', function () {
      console.log('第' + (i + 1) + '个按钮被点击')//i = 0
    })
}

当我们添加监听事件的时候,此时的i为0,所以此时event.target就是btns[0],点击按钮1后执行的回调函数里的内容也是打印“第1个按钮被点击”这句话,但是因为我们只是将其绑定到了click事件中,并没有触发。到这个阶段和我们的理解都没有偏差。
关键在于下一步,当我们循环到var i = 1的时候,我们还是聚焦于大括号里的内容:

	{
	var i = 1
	btns[i].addEventListener('click', function () {
      console.log('第' + (i + 1) + '个按钮被点击')//i = 1
    })
}

此时的i为1,监听的对象变成了btns[1],点击按钮2后回调函数里的控制台输出的内容应该是“第2个按钮被点击”,正是这个时候与我们理解出现不同的地方在于,在i为0的代码块里,控制台输出的内容也会变成“第2个按钮被点击”。
我们来分析原因,在我们的认知里,for循环中小括号的内容里定义的var i是全局变量,所以在第二个代码块里改变的i本质上改变的是全局变量下的i,也就是当var i = 1这一句代码执行的时候,全局变量下的i 也受到了i = 1的赋值操作,但原理真的是这样吗?
我们必须搞清楚这个所谓全局变量的i到底是为什么称为“全局变量的”。

	if(true){
		var i = 0
		}
	console.log(i)

很显然我们打印到i的值是0而不是i is not defined,这是因为if语句的块里没有作用域被劫持,在global里也能读到i这个变量,所以我们理解的for循环小括号里的i是全局变量,并非是他本身就在global环境下就存在var i = 0这样一个声明,而是因为local里的变量被global里的控制台输出所得到了。
先回到案例,我们引入第一种解决方法,使用立即执行函数,使用闭包来得到我们想要的效果。

解决方法一

  for (var i = 0; i < btns.length; i++){
    (function (i) {
      btns[i].addEventListener('click', function () {
        console.log('第' + (i + 1) + '个按钮被点击')
      })
    })(i)
  }

result2
我们按照刚刚的思路,用一种便于理解的思路来分析代码的执行过程。
还是当循环开始i的值为0时:

	{
	var i = 0
	var fn0 = function (i) {
      btns[i].addEventListener('click', function () {
        console.log('第' + (i + 1) + '个按钮被点击')
      })
    }
    fn0(i)
}

我们把一次性函数也当做声明后立即执行的函数来理解就很好理解了,其实在这里我们函数执行里的实参i和函数的形参i是两个不一样的变量,我们甚至可以将形参i换一个名字以便于理解。

	{
	var i = 1
	var fn0 = function (index) {
      btns[index].addEventListener('click', function () {
        console.log('第' + (index + 1) + '个按钮被点击')
      })
    }
    fn0(i)
}

当i为1的时候我们就很好理解了,此时将i重新声明为1,global下的i也变成了1,但是我们此时传进去的形参index因为函数的作用域变成了一个局部的变量他的值在传参的时候就确定了是1,之后循环到var i = 2的时候,里面的形参index也与这个index没有任何关系,因为他们都有自己的作用域,他们的关系只是自己作用域里刚好同名的变量而已。
我们用打断点的方式来支持我们刚刚对全局变量i的猜想。
在这里插入图片描述
在这里插入图片描述

可以相当明显的看出来,每一次循环的i都是在local下的变量,这样证实我们刚刚对于i作为全局变量的猜想是正确的:i并不是本身就在global里声明的变量,而是在local里声明,但因为没有块级作用域的概念而污染了全局。
我们在代码的最后加上一句控制台输出语句:console.log(i),输出的结果是循环外的i为5,与前面i是local下的变量,可以证实他污染到了全局。
在这里插入图片描述

解决方法二

我们使用es6新增的let关键字来解决,也可以从中看出一些这个作为var的完美替代品的let关键字的一些异同。

  for (let i = 0; i < btns.length; i++){
    btns[i].addEventListener('click', function () {
      console.log('第' + (i + 1) + '个按钮被点击')
    })
  }

我们只是将var换成了let,就足够得到我们想要的效果,我们也可以像之前一样逐次分析let的作用。
还是从当i的值为0开始:

	{
	let i = 0
	btns[i].addEventListener('click', function () {
      console.log('第' + (i + 1) + '个按钮被点击')//i = 0
    })
}

这个时候与var还看不出很明显的区别,但是当i的值为1时我们就能理解了。

	{
	let i = 1
	btns[i].addEventListener('click', function () {
      console.log('第' + (i + 1) + '个按钮被点击')//i = 1
    })
}

经过第二次对i的声明之后,因为let有块级作用域的概念,在大括号之外是无法访问到i的变量的。
我们用之前的if方法可以验证:

	if(true){
		let i = 0
		}
	console.log(i)

所以这里和解决方法一有一些异曲同工之妙,也就是let和立即执行函数都是让i变成了只在函数的大括号下的局部变量,用打断点的方式验证此时i也仍旧是local下的变量。
在这里插入图片描述

其实讨论到这里大家对let和var的理解在另一个角度也可以理解了,也就是无论是这两种哪一种声明方式,都是存在变量提升的,他们都是提升到了块的最顶端,但区别在于,因为var没有块级作用域的概念,所以全局变量也被污染了,在global环境下一样可以读到i的值。
而let的变量提升则使得在let之前的代码语句都陷入了暂时性死区(TDZ),使得代码失效了,但这并不是let没有进行变量提升。也因为let有块级作用域的原因,他劫持了所在块的作用域,使得他本质上作为local下的变量,在global下是无法读取到的。


总结

我猜想为了提高性能,let产生的i在循环结束之后就被垃圾回收机制所回收,但是因为本身是块级作用域,外部无法读取的更大原因是因为这个原因。所以更推荐大家使用es6新增的let和const的声明,也是对性能和变量污染问题的优化吧。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值