深入理解js中的异步与闭包

有一道非常有名的面试题,粗略如下:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<input type="text" id="input" value="">
<script>
var input = document.getElementById('input');
for(var i=0;i<3;i++){
setTimeout(function(){
input.value=i;
},2000)
}
</script>
</body>
</html>

请问页面发生了什么变化?
遥想当年,年轻羞涩的我奋力书写到:
输入框的值两秒后变成0,再隔两秒后变成1,再隔两秒后变成2。
头脑中还想着这题目,这么简单。
很明显,我们想要的效果不就是这样吗?
但计算机不是这么理解我们的代码的。
js中我们经常接触的异步场景是ajax,当一个响应不知道何时能返回的时候,
我们绑定了一个回调函数,响应何时返回,回调函数何时调用。
但js中的异步无处不在,setTimeout,setInterval都是异步。
他们之间的区别在于后者给了一个具体的时间。
所以我们可以这样理解js中的异步:
一个动作不在当下执行,而在未来的一个具体时间或不具体时间执行,我们叫做异步。
异步给我们带来了巨大的便利,但同时也给我们的代码带来了陷阱。
譬如上面的例子,你会发现输入框两秒后变成了3。
这根本不是我们想要的效果,为什么呢?
为了更明显看出背后的逻辑,我们把例子更改一下:

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

你会发现控制台两秒后输出了3个3。
很奇怪吧?
我们尝试理解计算机的逻辑:
第一次进来时,i=0;开启一个定时器,告诉计算机两秒后在控制台打印变量i的值;随后i++;
第二次进来时,i=1;告诉计算机在控制台打印变量i的值;随后i++;
第三次进来时,i=2;告诉计算机在控制台打印变量i的值;随后i++;
i=3;条件不成立,循环结束。
两秒钟后,定时器时间到了,响应之前的命令:在控制台打印变量i的值。
此时,变量i的值等于3;因为之前下了三次命令,所以控制台打印了3个3。
也就是说:我们是让控制台两秒后打印变量i的值,而不是现在。
等到两秒后,循环早已经结束,i=3,
此时打印变量i,当然等于3,因为之前循环了三次,所以打印了3个3。
如果你还不能理解,你可以转化下思维,把这当成ajax。

for(var i=0;i<3;i++){
$http.get('url').success(function(){
console.log(i);
})
}

假设我们的这个接口url响应数据的时间是两秒,循环的速度肯定快于接口,
所以等到接口响应后打印i的值,当然早就变成3。
那我们如何才能在回调函数中获取到对应的i值,而不是循环结束后的i值呢?
这时候我们需要引入闭包——closure。

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

用外部匿名函数+自动执行+内部函数构建一个闭包,在内部函数中获取外部函数传进来的i值。
结果是:两秒钟后控制台输出了0,1,2。
不用闭包,用forEach()也能实现:

var arr=[0,1,2];
arr.forEach(function(i){
setTimeout(function(){
console.log(i);
},2000)
})

结果是:两秒钟后控制台也输出了0,1,2。
所以你能看到for循环和forEach还是有区别的。
当然,到这里,还是没实现我们想要的效果:
输入框的值两秒后变成0,再隔两秒后变成1,再隔两秒后变成2。
即使我们把setTimeout改成setInterval,结果依然不是我们想要的。

for(var i=0;i<3;i++){
(function(i){
setInterval(function(){
console.log(i);
},2000)
}(i)
}

结果是:两秒钟后控制台输出了0,1,2,再隔两秒后又输出了0,1,2,周而复始。
在下篇文章中我会介绍如何实现我们想要的效果,敬请期待。