文章目录
问题
基础知识
setTimeout()
方法是window的一个方法,设置一个计时器,一旦计时器到期,该计时器就会执行一个函数或指定的一段代码。
setTimeout(fun|code, [delay], [arg1],[arg2],...);
fun|code: 一个回调函数或一段代码
delay:延迟时间(ms)
arg1,arg2,...: 作为回调函数的参数
setTimeout()
允许一个字符串作为第一个参数,而且js内部将会调用eval()函数用来动态执行一段字符串脚本
eval()函数可以接受一个字符串str作为参数,并把此str当做一段javascript代码去执行,
如果str执行结果是一个值则返回此值,否则返回undefined。
如果参数不是一个字符串,则直接返回该参数。
注意:
setTimeout()
的第一个参数不管是一个函数还是一段代码,
如果传入的是引用函数地址,则不会立即执行;
如果传入的是执行函数,则会立即执行,不受到setTimeout()
函数的制约;
情况1:
输出的内容不是0、1、2、3、4、5、6、7、8、9
for(var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
//输出: 1s后连续输出10个10
说明:
- 声明变量的var使得i成为一个全局变量,for循环是同步任务
setTimeout
是异步执行,1s后往任务队列里面添加一个任务,只有主线上的同步任务执行完,才会执行任务队列里的任务,for
循环每执行一次,setTimeout
都会执行一次,但是里面的回调函数() => {console.log(i);}
没有被执行,而是被放到任务队列里,等待执行,for循环了10次,回调函数就放入10次- 1s后,当主线任务完成后,
i=10
,所以此时再去执行任务队列里的任务时,有10个{console.log(i);
因此连续输出10个10
情况2:
for(var i = 0; i < 10; i++) {
setTimeout(console.log(i), 4000);
}
//0,1,2,3,4,5,6,7,8,9
说明:
console.log()
是立即执行函数,不是一个函数地址,与for循环都是同步任务,因此会立即执行
情况3:
for(var i = 0; i < 10; i++) {
setTimeout('console.log(i)', 4000);
}
//输出:4s后连续输出10个10
eval(console.log) => ƒ log() { [native code] }
eval(console.log()) => undefined
说明:
console.log()
加上引号以后,就变成了一个引用函数地址,即函数为 console.log
,其中的参数为i
。
解决
1、使用let构建块级作用域
将for循环里的变量i绑定到循环体的每一次迭代中
for(let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
2、使用闭包,构建块级作用域
通过闭包,将i的变量驻留在内存中,当输出j时,引用的是外部函数的变量值i,i的值是根据循环来的,执行setTimeout时已经确定了里面的的输出了。
> 方式一:
for(var i = 0; i < 10; i++) {
((j) => {
setTimeout(() => {
console.log(j);
}, 1000);
})(i);
}
> 方式二:
for(var i = 0; i < 10; i++) {
closure(i);
}
function closure(i) {
setTimeout(() => {
console.log(i);
}, 1000);
}
3、使用立即执行函数
setTimeout
里面的第一个参数是立即执行函数,是同步任务,与for循环同步执行,不受setTimeout
的制约
//立即执行函数:是一种既不需要函数名同时能够自动执行的函数。
(function(){
})();
(function(){
}());
for(var i = 0; i < 10; i++) {
setTimeout(((i) => {
console.log(i);
})(i), 1000);
}
//setTimeout里面的第一个参数console.log()是IIFE
for(var i = 0; i < 10; i++) {
setTimeout(console.log(i), 1000);
}
4、使用setTimeout第三个参数
第三个参数:就是给setTimeout第一个函数的参数,即作为回调函数console.log
或者(j) => {console.log(j);}
的参数传入
for(var i = 0; i < 10; i++) {
setTimeout(console.log, 1000,i);
}
for(var i = 0; i < 10; i++) {
setTimeout((j) => {
console.log(j);
}, 1000,i);
}
5、利用try/catch
- catch中的参数会形成一个块级作用域
- 但是如果在catch内部进行var声明,依然是声明在全局作用域中
for(var i = 0; i < 10; i++) {
try {
throw i;
} catch(i) {
setTimeout(() => {
console.log(i);
}, 1000)
}
}
6、promise
for(var i = 0; i < 10; i++) {
Promise.resolve(i).then((i) => {
setTimeout(() => {
console.log(i);
}, 1000);
});
}
说明:
Promise.resolve() ==> new Promise(() => {resolve()});
resolve是成功状态,后面的then里的参数会使用resolve里的参数,因此相当于形成了一个块级作用域。