经典闭包循环
初始题目
1
2
3
4
5
6for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);上面的代码等同于:
1
2
3
4
5
6
7for (var i = 0; i < 5; i++) {
function foo() {
console.log(new Date, i);
}
setTimeout(foo, 1000);
}
console.log(new Date, i);解析:setTimeout 里面传入的第一个匿名函数,等价于在 setTimeout 语句外面定义的一个函数。所以它的闭包范围是变量 i 所在的作用域,所以可以访问到 i。由于 i 是外面的变量,所有的 setTimeout 里面的函数都会指向同一个 i。
闭包改造
普通闭包
1
2
3
4
5
6
7
8for (var i = 0; i < 5; i++) {
(function(j) { /* j = i */
setTimeout(function() {
console.log(new Date, j);
}, 1000);
})(i);
}
console.log(new Date, i);解析:每次把新的变量 i 传进一个匿名的立即执行函数,每次 j 都能得到不同的 i ,因为 j 在匿名函数的作用域内,函数的执行作用域每执行一次都会重新生成,所以每次的 j 都不是同一个。
传参法(其实是把闭包匿名的立即执行函数单独拿出来,然后在 for 循环的时候,向这个函数传递参数)
1
2
3
4
5
6
7
8
9
10function wrapper(j) {
function foo() {
console.log(new Date, i);
}
setTimeout(foo, 1000)
}
for (var i = 0; i < 5; i++) {
wrapper(i)
}
console.log(new Date, i);注意:这里有一个容易犯错的点,setTimeout 的第一个参数要求的是一个函数的引用,而不是执行一个函数,所以只能传入函数名 foo 的形式,而不能传入 foo(a,b),这也意味着不能在 foo 上直接传参。所以,要传参的话,在 setTimeout 外面再包裹一层函数,然后定制编写 foo 函数。
再补充一个重要知识点,如果一定要在 foo 中传参,又不想用闭包,可以使用 setTimeout 的第3个参数,从第3个参数往后的参数,都会传入 foo 里作为形参使用。
所以,上面的代码也可以写成:
1
2
3
4
5
6for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(new Date, j); /* 可以立即输出01234 */
}, 1000 ,i);
}
console.log(new Date, i);如果硬是在 setTimeout() 中传入的 foo() 的形式,那么只会以正常任务的方式立即执行 foo(),而不会放入任务队列里去,也就是定时器失效。
Promise
上述循环用 ES6 和 Promise 实现如下:
1 | var tasks=[]; |
解析:关于任务队列:
- 首先第一趟主任务队列走下来,执行了设置定时任务,将promise对象放入tasks数组,并设置好then回调的工作。
- 然后第二趟,执行定时任务队列,运行consolo.log语句,
- 然后遇到resolve,需要调用相应的then里面的回调语句(如果有的话)。
- 但是注意,这里调用then的时机,是在本次任务的主代码执行完毕后,也就是说,如果setTimeout语句中的resolve()后面还有执行语句,要先执行那些语句,最后才执行resolve对应的then回调。
- 要确定resolve相应的回调语句的执行顺序,可以执行以下上述代码(将代码里的两句console.log去掉注释即可):
最后强调一遍,resolve的回调函数是在本轮“事件循环”结束时执行,setTimeout(fn, 0)在下一轮“事件循环”开始时执行。
举例说明:
1 | setTimeout(function () { |
- 作者:栗子酥小小
- 链接:原文地址
- 來源:简书
- 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。