问题引入
🤔实现一个方法:每隔一秒打印打印0,1,2,3,4五秒后结束.
我的第一次实现如下:
for(var i=0; i<5; i++) {
setTimeout(()=>{console.log(i)}, i*1000)
}
结果不尽人意! 每隔1秒打印一个5。
🤔为什么定时器中的箭头函数中无法读取到正确的i,而定时器的第二个参数可以读到第正确的i呢?
此处需要重新理解箭头函数
😉chat老师这样说:
箭头函数是没有自己的
this对象和arguments对象的,它们的this和arguments对象都是从父级作用域中继承而来的。换句话说,箭头函数的上下文是继承自外层函数的上下文,它们没有自己的作用域链。
在这个for循环中,定时器中的箭头函数继承自它被创建时所处的块级作用域中的i,
且在此在for循环中, i是通过var关键字定义的,
这代表在整个for循环的过程中, 每一次迭代的i都指向同一个地址。
因此,当箭头函数在循环中被创建时,它们继承的是循环末尾的 i 值,而不是在循环内部的值。
第二次修改:
for(let i=0; i<5; i++) {
setTimeout(()=>{console.log(i)}, i*1000)
}
😁此时结果正确,可以每隔1秒打印出0,1,2,3,4结束。
🤔为什么let可以了呢?
每次循环,js都创建了一个新的块级作用域,因此每次i都在这个新的作用域内被重复声明,每次循环中的i都是相互独立,互不干扰的。
此时箭头函数继续去当前作用域寻找i的值时,它处在当前块级作用域中,所以他找到的是此次循环对应的i,而不是最后一次的i。
若用
var定义i,是否可以实现此功能?
此问题的关键是如何让定时器回调方法中可以读取到正确的i值。
✔ 有以下办法可以解决:
1. 使用立即执行函数
立即执行函数用来创建一个新的变量作用域,从而避免变量污染全局作用域,
同时可以模拟块级作用域。
正好可以应用在此场景下!
for(var i=0; i<5; i++) {
(function(i) {
setTimeout(()=>{console.log(i)}, i*1000)
})(i)
}
立即执行函数创建了一个新的作用域,这个作用域中的 i 变量是外部 for 循环中的 i 的一个副本。因此,每次循环迭代时,i 的值都会被传递给这个立即执行函数,并在这个函数内部创建一个新的变量,这个变量仅在当前迭代中有效,并且不会被后续迭代所影响。
2. 普通函数也可以😅
for(var i=0; i<5; i++) {
function print(i) {
setTimeout(()=>{console.log(i)}, i*1000)
};
print(i);
}
每次循环时,函数 print 都会创建一个新的变量作用域,这个作用域中包含了一个变量 i,这个变量 i 是外部 for 循环中的 i 的一个副本。由于定时器中的箭头函数引用了函数 print 中的参数 i,因此它能够读取到这个变量 i 的值。
2. 利用setTimeout的参数
for(var i=0; i<5; i++) {
setTimeout(function(i) {
console.log(i)
}, 1000*i, i)
}
事件循环
进入主题,为什么提到事件循环?
for(var i=0; i<5; i++) {
console.log(i)
}
执行以上for循环,结果按照预期,打印0,1,2,3,4结束。
为什么加了setTimeout后就不行了呢?
像这样!
for(var i =0; i<5; i++) {
setTimeout(function(){
console.log(i)
}, i*1000)
}
结果打印5,5,5,5,5结束。
发现是js事件循环机制造成的 (复习读程的过程中看事件循环读的出来,脱离事件循环的环境啥都忘了😅)
这段代码的执行过程如下:
-
for循环开始,变量i被初始化为0,进入循环体。 -
在循环体中,
setTimeout函数被调用,创建了一个定时器,回调函数被放入任务队列中。注意❗❗❗
在计算
setTimeout的第二个参数时,变量i的值已经被捕获并保存在了一个闭包中。 -
i的值递增到5,循环结束。 -
此时,
setTimeout函数创建的所有定时器都已经到达了指定的延迟时间,回调函数开始依次执行。注意,此时i的值已经是5。 -
回调函数中的
console.log(i)语句输出的是5。