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

简介: 最近,有一道JavaScript面试题挺流行的,很多朋友去面试的时候都遇到了。这道题目大致是这个样子的:以下这段代码执行后,结果为什么不是依次输出0到9?如果要让它实现这样的输出,你会怎么来修改这段代码?for (var i = 0 ; i <...

最近,有一道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。

目录
相关文章
|
6月前
【变态面试题】【两种解法】不能创建临时变量(第三个变量),实现两个数的交换
【变态面试题】【两种解法】不能创建临时变量(第三个变量),实现两个数的交换
51 0
【变态面试题】【两种解法】不能创建临时变量(第三个变量),实现两个数的交换
|
3月前
|
Java
【Java基础面试七】、请介绍一下实例变量的默认值
这篇文章介绍了Java中实例变量的默认值:引用数据类型的默认值是null,而基本数据类型的默认值根据其类型分别是0、0L、0.0F、0.0、'\u0000'和false。
【Java基础面试七】、请介绍一下实例变量的默认值
|
3月前
|
Java 编译器
不同变量的赋值时机 | 常见的面试题 | 静态代码块
这篇文章讨论了Java中不同变量的赋值时机,包括基本数据类型、引用数据类型、类变量、实例变量和局部变量,并解释了静态代码块、代码块和构造函数的执行顺序。
不同变量的赋值时机 | 常见的面试题 | 静态代码块
【IO面试题 五】、 Serializable接口为什么需要定义serialVersionUID变量?
serialVersionUID用于标识类的序列化版本,确保在反序列化时类的版本一致性,避免因类定义变更导致的不兼容问题。
|
4月前
|
存储 设计模式 监控
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
49 0
|
4月前
|
存储 JavaScript 前端开发
面试官:JS中变量定义时内存有什么变化?
面试官:JS中变量定义时内存有什么变化?
40 0
|
6月前
|
Python
2024年最新【Python】变量 的定义和使用,阿里巴巴蚂蚁金服面试流程
2024年最新【Python】变量 的定义和使用,阿里巴巴蚂蚁金服面试流程
2024年最新【Python】变量 的定义和使用,阿里巴巴蚂蚁金服面试流程
|
6月前
|
索引
【ES6新语法】let、const、var的区别,你学会了面试官没话说
【ES6新语法】let、const、var的区别,你学会了面试官没话说
|
6月前
|
存储 JavaScript 前端开发
每日一道javascript面试题(九)函数的参数可以和函数体中的变量重名吗
每日一道javascript面试题(九)函数的参数可以和函数体中的变量重名吗
|
6月前
|
JavaScript 前端开发
每日一道javascript面试题(六)有var和无var
每日一道javascript面试题(六)有var和无var
下一篇
无影云桌面