Bootstrap

面试题27:在for循环中运行setTimeout问题(for(var i=0;i<5;i++){ setTimeout(function(){ console.log(i); },1000))

目录

疑惑:

重点讨论一下这个现象

解决方法:

一 、利用setTimeout的第三个参数var ,将i作为参数传递给setTimeout

二、使用IIFE(立即执行的匿名函数)

三、使用bind方法

四、es6中的let声明的变量是具有块级作用域

五:用立即执行函数也可以


疑惑:

重点讨论一下这个现象

for(var i=0;i<10;i++){
    setTimeout(function(){},console.log(i),i*1000,i);
}

答案:0、1、2、3、4、5、6、7、8、9

 再比较一段代码

for(var i=0;i<10;i++){
        setTimeout(function(){
         console.log(i);  
        },i*1000);
    }
答案:10、10、10、10、10、10、10、10、10、10

发现什么奇怪的问题了吗?
为什么第二段代码的结果是10个10呢?

原因是for循环中使用setTimeout涉及到了异步机制。说到异步机制那就要说一下JS的运行机制:

JS是单线程环境,也就是说代码的执行是从上到下,依次执行。也就是同一个时间只能做一件事。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。JavaScript将所有任务分成两种,一种是同步任务,另一种是异步任务。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。在所有同步任务执行完之前,任何的异步任务是不会执行的。

异步执行的运行机制如下:
1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
4. 主线程不断重复上面的第三步。
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会循环反复。

一般来说,有以下四种会放入异步任务队列:
setTimeout和setlnterval
DOM事件
ES6中的Promise
Ajax异步请求

回到开头的代码中:
 

for(var i=0;i<10;i++){
        setTimeout(function(){
         console.log(i);  
        },i*1000);
    }
答案:10、10、10、10、10、10、10、10、10、10

代码中的for循环是个同步任务setTimeout是个异步任务所以把for循环执行完毕后(i=10)才会执行setTimeout()函数,所以导致输出了10个10的结果。

具体执行步骤如下:
1.for循环为同步任务,先执行产生了十个定时器。
(由于这里的for循环不是独立的代码块,声明的 i 为全局变量,遍历完成相当于全局环境i=10;)
2.同步任务for遍历完了再执行异步任务的定时器。
(此时定时器运行环境与for循环同级,即在全局环境i=10下运行,因此输出10个10)

那么怎么才能输出正确答案0、1、2、3、4、5、6、7、8、9而不是连续的10个10呢?


主流的方法是用Js的闭包来实现这个问题:因为闭包里setTimeout()并不是暴露在全局环境下,而是在闭包的独立作用域中。(这种方法当然是可行的,且兼容几乎所有浏览器,但是闭包写起来有点繁琐…有没有更简单的方法呢?答案是~有的!)

回到我们的问题结果出现本身:问题代码里for循环不是独立的代码块,导致了所声明的i为全局变量,进而导致了定时器执行时候的i为遍历后的i=10。

解决方法:将变量i改为let声明的变量而不是全局变量,此时的for循环就会变成是一个单独的作用域,作用与闭包形成的块级作用将相同,因此能够输出想要的结果。

温馨提示:ES6中的属性方法let目前只支持现代浏览器,所以如果考虑浏览器兼容问题的小伙伴还是乖乖用闭包的方法吧。

那么又有小伙伴有疑问了,既然在for循环中运行setTimeout()涉及到了异步,那为什么第一段代码的结果不是连续的10个10呢?

这就涉及到了console.logconsole.log()的区别了。
先用eval()测试一下:console.log 与console.log()

 此处顺便一提eval()函数的用法:eval(str),此函数可以接受一个字符串str作为参数,并把此str当做一段javascript代码去执行,如果str执行结果是一个值则返回此值,否则返回undefined。

 参考的博客里对console.log 与console.log()的区别是这么写的:
console.log是一个function 函数名,而console.log()是立即执行的,也就是IIFE,所以console.log()是同步任务跟for循环是同步执行的,而setTimeout()是异步任务需要等到主线程的同步任务执行完毕后才能执行。


海量查阅后自我总结如下:
针对上面两端代码,每段代码的每次for循环其实都会执行setTimeout(),setTimeout() 会判断第一个参数是否是[function],如果是,则执行异步任务。如果不是,则会尝试将它当做字符串处理。所以第二段代码setTimeout里面的function被当成异步任务不会立刻执行,而是被放入任务队列。而第一段代码中console.log(i)被当作字符串处理(即setTimeout里没有回调函数),所以相当于是跟着for循环同步执行的而不需要等到for都遍历完成(i=10)再执行计数器。
 

 再来对比第三段代码:

 for(var i=0;i<10;i++){
     setTimeout("console.log(i)",1000);
    }

答案:10、10、10、10、10、10、10、10、10、10
 

与第一段代码比较下:

for (var i=0; i<10; i++) { 
    setTimeout(console.log(i), i*1000 );
    }

答案:0、1、2、3、4、5、6、7、8、9

for (var i=0; i<10; i++) { 
    setTimeout(console.log(i), i*1000 );
    }

答案:0、1、2、3、4、5、6、7、8、9


原文链接:https://blog.csdn.net/weixin_44066534/article/details/106433890

解决方法:

var 定时器时间为1000
setTimeout是异步执行,在执行setTimeout时,i已经为5了

for(var i = 0; i < 5; i++) {
	setTimeout(
		function() {
			console.log(i)	// 5 5 5 5 5 
		}, 1000)
}

var 定时器时间为0

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

如果想要输出 0 1 2 3 4

一 、利用setTimeout的第三个参数
var ,将i作为参数传递给setTimeout

for(var i = 0; i < 5; i++) {
	setTimeout(
		function(i) {
			console.log(i)	// 0 1 2 3 4 
		}, 1000, i)
}

二、使用IIFE(立即执行的匿名函数)闭包

for(var i = 0; i < 5; i++) {
	(function(i){
		setTimeout(
			function() {
				console.log(i)	// 0 1 2 3 4 
			}, 1000)
	})(i)
	
}

三、使用bind方法

bind()方法创建一个新的函数,在bind()被调用时,这个新函数的this被bind的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。

bind MDN详细解释

for(var i = 0; i < 5; i++) {
	setTimeout(
		function(i) {
			console.log(i)	// 0 1 2 3 4 
		}.bind(null,i), 1000)
}

四、es6中的let声明的变量是具有块级作用域

let 定时器时间为0

for(let i = 0; i < 5; i++) {
	setTimeout(
		function() {
			console.log(i)	// 0 1 2 3 4 
		}, 0)
}

let 定时器时间为1000 都能输出同样结果,只是时间问题
let 语句声明一个块级作用域的本地变量,并且可选的将其初始化为一个值。
let声明的变量只在其声明的块或子块中可用,这一点,与var相似。二者之间最主要的区别在于var声明的变量的作用域是整个封闭函数。

for(let i = 0; i < 5; i++) {
	setTimeout(
		function() {
			console.log(i)	// 0 1 2 3 4 
		}, 1000)
}

五:用立即执行函数也可以

for (var i = 0; i < 5; i++) { 
 	(function(i){      //立刻执行函数
		setTimeout(function (){
			console.log(i);  
		 },1000);  
 	})(i);	
}

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;