提出问题
我来拆解一下这个循环与闭包问题
例子:
for(var i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i);
}, i*1000);
}
在我的设想里这段代码会是间隔1秒按顺序输出1-5,然后循环结束,但实际情况会是每间隔一秒打印一次6,为什么会出现这种情况实际上这个问题不仅仅是作用域问题同时也是一个同步与异步的问题
先将这一组代码改变另一种形式,让我们更好的理解
function timer(){
console.log(i);
}
var i = 1
for(;i <= 5;){
setTimeout(timer,i*1000);
i++;
}
这样拆分代码问题就十分明显了,
1. var i = 1 是将该变量定义在全局作用域中了, 函数timer也是在全局作用域中,在函数timer中打印i自然会会去全局作用域中寻找
2. 我们将函数timer作为计时器setTimeout的回调函数传入进去,此时就出现了第一问题是闭包,函数timer会一直持有访问全局作用域变量的能力,也就是访问变量i的能力
3. 第二个问题是同步与异步任务,for是同步执行的任务,计时器setTimeout是异步执行的任务,在事件循环中会优先执行同步任务当同步任务全部执行结束之后才会执行异步任务
每一次循环都会创建一个计时器setTimeout,第一个计时器的延迟时间为1秒,第二个为2秒,一次类推,当循环结束(同步任务执行完成),开始执行计时器setTimeout回调函数的内部代码,此时代码是同步的所以会立即执行
但是此时回调函数timer形成了闭包会向全局作用域中寻找变量i,而这时的变量i已经变成了6,
所以间隔12345秒打印一次6
下面使用伪代码简单的表示一下
var i = 1
for(; i <= 5;){
setTimeout(timer,1*1000);
1++;
}
i = 2;
for(; i <= 5;){
setTimeout(timer,2*1000);
2++;
}
i = 3;
for(; i <= 5;){
setTimeout(timer,3*1000);
3++;
}
i = 4;
for(; i <= 5;){
setTimeout(timer,4*1000);
4++;
}
i = 5;
for(; i <= 5;){
setTimeout(timer,5*1000);
5++;
}
i = 6;
i > 5 条件不成立 循环结束
解决问题
第一种
昏招1: 不使用异步任务,采用同步任务解决(也就是堵死JS主线程,你别管他合不合理,你就说它实没实现)
function late(lateTime){
let startTime = new Date().getTime();
while (new Date().getTime() < startTime + lateTime){
// console.log(new Date().getTime());
}
}
console.log(1);
late(1000);
console.log(2);
late(1000);
console.log(3);
late(1000);
console.log(4);
late(1000);
console.log(5);
// 记得先打开控制台再执行函数和打印
昏招2: 使用迭代器(我愿称之为昏招1自动升级版)
var a = 0;
function *foo(){
while(true){
if(a >=5 ){
return
} else {
a++;
}
yield a;
}
}
let res = foo();
let t = setInterval(()=>{
if(a>=5){
clearInterval(t);
t = null;
} else {
var { value } = res.next();
console.log(value);
}
},1000);
第二种: 由上面的结论我们可以知道,同步与异步任务执行的顺序导致所有定时器执行的回调函数都会持有同一个全局作用域,那我们只需要让每个计时器的回调函数拥有一个单独的作用域即可
选择使用let声明变量的方式
for(let i = 1; i <=5; i++){
setTimeout(function timer(){
console.log(i);
},i*1000);
}
选择使用立即执行函数的方式
for(let i = 1; i <=5; i++){
;(function(j){
setTimeout(function timer(){
console.log(j);
},j*1000);
})(i);
}
选择使用定时器的第三个参数
for(var i = 1; i <=5; i++){
setTimeout(function timer(i){
console.log(i);
},i*1000,i);
}
这种方式大概就是在每次循环结束定时器开启之前获取当前的i传入回调函数中,跟立即执行函数的方式差不多
其他的方式我就不知道了,各位如果知道请自行发挥
参考书籍
《YOU DON'T KNOW JavaScript》