发布于 

对一道循环计数题进行解剖

题如下:

1
2
3
4
5
6
7
8
9
//想要效果:在for循环内,使用setTimeout延迟两秒后,依次打印i的值:0,1,2,3,4

for(var i=0;i<5;i++){

setTimeout(()=>{
console.log(i)
},2000)

}

上述代码最终的执行结果:延迟2秒后,在控制台中打印了五个5,这并不是我们预期想要的结果

这是为什么呢?进行分析:

  1. 这里的var声明不具备块级作用域,变量i被声明为全局变量

  2. 这段代码包含了同步代码和异步代码,为什么会产生异步,异步又如何执行

    首先JS是在单线程的环境下工作的,为了避免某一段JavaScript代码长时间运行,阻塞后面代码执行,导致其他任务无法执行(例如:浏览器无响应、假死),JavaScript将任务的执行模式分成两种:同步和异步

    • 同步模式:在主线程(执行栈)排队等待的任务,只能等待前一个完成才会执行完毕后才会执行下一个任务

    • 异步模式:不进入主线程而进入”消息队列“的任务(异步任务),会在”消息队列”中排队等待,当主线程中的任务运行完了,才会从”消息队列“取出异步任务放到主线程执行,执行完主线程,又再从”消息队列“取出任务执行,如此反复(这就是常说的eventloop)

    那又怎样才算异步任务,如何区分呢?

常见的异步任务有:Ajax请求、setTimeout、Promise.then等 (异步任务又细分为宏任务和微任务,这里就不细说了)

那题中的代码执行顺序就可以这样解释:

开始进入for循环

在执行for内部时,setTimeout会被认为是异步任务,不会立即执行,而是推到消息队列排队等待,等待主线程的代码(同步代码)执行完后,再从消息队列出列,放到主线程去执行

主线程的代码:执行for循环这一块,当变量i不满足条件跳出循环时,也就是同步代码执行完毕的时候,此时的变量i的值为5

主线程代码执行完后开始执行异步的代码:setTimeout内的箭头函数

在执行箭头函数时,由于它内部并没有声明i变量,就会沿作用域链找,最终在全局作用域下,找到了变量i(i的值已经变为5了),每一次打印变量i时,都是在打印全局作用域下的i,也就是说它们共享同一个i,所以就打印了五个5

解决

一:使用闭包

闭包:由函数及声明该函数的词法环境组成,该环境包含了闭包函数创建时作用域内所有的局部变量,闭包维持了对它的词法环境的引用,简而言之,闭包能让我们从内部函数访问到外部函数的作用域

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function makeFunc(){
var name = "zhangs";

function displayName(){
console.log(name)
}

return displayName;
}

var myFunc = makeFunc()
myFunc()
//执行结果:控制台打印了“zhangs”

makeFunc函数内有一个局部变量name和声明了一个displayName函数,并且makeFunc函数执行时,会返回displayName函数

myFunc就是在makeFunc执行后,接收了它的返回值,一个对displayName函数实例的引用

myFunc方法执行就是在执行displayName方法,那为什么console.log(name),会输出zhangs呢?

进行分析:
按道理来说,makeFunc函数执行完后,它的局部变量name会被销毁才对,然而代码却按照预期执行。
原因就在于JavaScript中的函数会形成闭包,displayName就是一个闭包函数,displayName维持了对它的词法环境(变量name就存在其中)的引用,就算makeFunc执行完毕,它的局部变量name也不会被销毁,因为name被displayName引用了。

回到开头的循环计数题,我们就可以用闭包实现我们想要的效果

1
2
3
4
5
6
7
8
9
10
11
12
for(var i=0;i<5;i++){

(function(j){

setTimeout(()=>{
console.log(j)
},2000)

})(i)


}
二:利用块级作用域(let)

ES6新增的let具有块级作用域,也可以用它进行循环计数

值得注意的是:const也具有块级作用域,但它不能用来计数(原因:const i = 0,i的值不允许再改变,i相当于常量)

1
2
3
4
5
6
7
8
for(let i=0;i<5;i++){	//此时用let声明的i不会成为全局变量,在外部无法访问到i

setTimeout(()=>{
console.log(i)
},2000)

}

总结:一道循环计算题涉及了以下知识:

  1. var、let和const的区别
  2. 同步模式、异步模式及异步代码如何执行
  3. 闭包