由一道关于变量作用域的面试题,来加深对var和let的理解

最近,有一道JavaScript面试题挺流行的,很多朋友去面试的时候都遇到了。这道题目大致是这个样子的:

以下这段代码执行后,结果为什么不是依次输出0到9?如果要让它实现这样的输出,你会怎么来修改这段代码?

for (var i = 0 ; i < 10; i++) {
  setTimeout(function () {
    console.log(i)
  })
}

那让我们先来看一看,在这段代码中打印变量i的最终输出结果到底会是什么呢?待一阵十指乱动,风一般的敲出执行代码的命令,只见屏幕一闪,亮出十行大字:

10
10
10
10
10
10
10
10
10
10

What?! 它输出的居然是10个10而不是更贴近我们第一感觉的0到9,这是怎么回事儿?又是一个什么坑......还能不能好好的写JavaScript了......

原因分析

其实,这个锅也不能全由JavaScript来背,有可能是你没有完全理解JavaScript导致的。产生这个运行结果的关键点就在于for语句中的var i = 0;这句变量声明代码。

我们都知道,var是用来声明变量的,并且我们通常也知道,一个语句从哪里开始声明就会在哪里开始被处理。但是var是JavaScript语法中的一个例外!我们来看一下Mozilla官方文档中对var的定义:

var变量声明,无论发生在何处,都在执行任何代码之前进行处理。 用var声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,也可以是声明在任何函数外的变量。如果你重新声明一个 JavaScript 变量,它将不会丢失其值。

由于上述定义的原因,var变量声明(以及其他声明,比如函数声明)总是在任意代码执行之前处理的,所以在代码中的任意位置声明变量总是等效于在代码开头声明。这意味着变量可以在声明之前使用,这个行为叫做变量提升(Hoisting)。变量提升就像是把所有的变量声明移动到函数或者全局代码的开头位置:

bla = 2
var bla

// 可以理解为:
var bla
bla = 2

因此对于我们这道题,变量i的声明就相当于提前到了for语句的外面,相应的,变量i的作用域范围也同时扩大到了for语句的外面,与以下的写法相互等效:

var i = 0;

for (; i < 10; i++) {
  setTimeout(function () {
    console.log(i)
  })
}

另外一点,我们得明白setTimeout()的运行时机:它总是在当前的同步代码执行完成后开始运行。可以在前面的代码中加入一些log进行跟踪并验证这一点:

var i = 0;

for (; i < 10; i++) {
  console.log('+++++', i)

  setTimeout(function () {
    console.log(i)
  })
}

执行这段代码后的结果:

+++++ 0
+++++ 1
+++++ 2
+++++ 3
+++++ 4
+++++ 5
+++++ 6
+++++ 7
+++++ 8
+++++ 9
10
10
10
10
10
10
10
10
10
10

由此可见,当开始执行setTimeout()中的代码时for循环外面的变量i就已经变成了10,使用console.log(i)从作用域查找到的i值就是10,因此十次setTimeout()中的代码就都打印出了10

解决方式

原因找到了,罪魁祸首说到底就是由于var变量的作用域特性以及作用域范围导致的。那解决这个问题的关键点还是在怎么控制变量的作用域。

方法一

要控制变量的作用域,最常见的手段,就是使用函数闭包将变量值封闭在指定的作用域内。

我们可以在setTimeout()的外面进行一层简单的包装来形成闭包,达到将每次循环时的i值封闭在闭包内部:

for (var i = 0 ; i < 10; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i)
    })
  })(i)
}

这样的话,在setTimeout()中查找变量i的时候,就会获取到封入闭包并以参数形式传入的参数i了。

方法二

除了函数闭包,我们还可以使用的解决方案,就是ES6中新引入的let变量声明。与var不同的是,由let声明的变量的作用域是只在其声明的块或子块中可用,所以它被称为块级作用域变量。

我们这道题的代码只要做很小的修改,只需要将var替换成let,就能如我们期望的那样工作了:

for (let i = 0 ; i < 10; i++) {
  setTimeout(function () {
    console.log(i)
  })
}

使用了let后,变量i的作用域被限定在for语句块以及子块setTimeout()中,并且:

子块中的变量值是该子块产生时的那个值

是不是觉得let变量的作用域关系比较清晰?在现在的实际开发中,我们也更推荐使用let来替代var进行变量声明,它会使你的代码更清晰更简化,不容易出bug。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏达摩兵的技术空间

前端面试题分享001

解释 :要注意的是函数中的this与运行环境强相关,与定义环境不相关。所以下面的代码段中,当直接通过对象属性方法中去调用时,其都可以访问到对象的属性,但是当其变...

9640
来自专栏大前端_Web

ECMAScript 6笔记(let,const 和 变量的解构赋值)

版权声明:本文为吴孔云博客原创文章,转载请注明出处并带上链接,谢谢。 https://blog.csdn.net/wkyseo/articl...

16350
来自专栏西安-晁州

golang学习之defer

golang中的defer通常用于执行一些资源释放性操作,比如open/close、connect/disconnect、lock/unlock等,对defer...

21700
来自专栏十月梦想

JavaScript中类的创建以及类的传参

在之前(ES2015)以前我们常用构造函数来搞定一个事物类,通过new 这个构造函数实现类的功能!在ES6(ES2015)中已经可以使用类,下面我们看一下类如何...

11920
来自专栏小詹同学

Python系列之——字符串格式化(xiaozhan is a boy of 22 years old.)

不知道小伙伴有没有遇到过字符串输出有格式要求的情况呢?今天小詹学习分享一波python的字符串格式化的方法。学以致用,首先我们得明确为什么要格式化字符串输出,以...

7320
来自专栏前端说吧

JS-提取字符串—>>普通方法VS正则表达式

39360
来自专栏C++

python笔记:#011#循环

21120
来自专栏繁花云

liunx下sed命令的用法

单引号里面,s表示替换,三根斜线中间是替换的样式,特殊字符需要使用反斜线”\”进行转义,但是单引号”‘”是没有办法用反斜线”\”转义的,这时候只要把命令中的单...

8300
来自专栏Golang语言社区

Golang 语言--map 用range遍历不能保证顺序输出

按照之前我对map的理解,map中的数据应该是有序二叉树的存储顺序,正常的遍历也应该是有序的遍历和输出,但实际试了一下,却发现并非如此,网上查了下,发现从Go1...

43680
来自专栏恰童鞋骚年

正则表达式30分钟入门教程--deerchao

原文地址:http://www.cnblogs.com/deerchao/archive/2006/08/24/zhengzhe30fengzhongjiaoc...

26940

扫码关注云+社区

领取腾讯云代金券