js经典面试题:setTimeout+for循环组合,使用闭包循环输出1,2,3,4,5

这段时间重新温习了一下js中的基础知识:内存空间,执行上下文,变量对象,作用域,闭包,函数调用栈,队列等等。当看到下面这道熟悉的经典for循环题目时,发现还有很多知识点理解的不到位,于是花时间利用以上知识,重新进行了对这道题目的深入剖析。

如下,利用闭包,让循环输出的结果为1、2、3、4、5

for(var i=1; i<=5; i++){
	setTimeout(function timer(){
		console.log(i);
	},i*1000)
}

不卖关子,以上代码执行后每隔1秒输出一次6,共输出5次。如下:
js经典面试题:setTimeout+for循环组合,使用闭包循环输出1,2,3,4,5_第1张图片
首先来看一下代码的执行顺序:

看代码执行的顺序就可以知道,console.log(i)并没有在每次for循环的时候执行,而是在for循环的所有条件判断执行完成之后才开始执行,而这时候,i 的值已经变为了6,而console.log(i)输出的 i 就是for循环结束之后的 i 的值,所以会输出5次6。i 定义的是一个全局变量,for循环每次获取的 i 值都会覆盖上次获取的 i 的值。

那为什么setTimeout一定要等到for循环执行完成之后再执行呢?

这就与javascript中的事件循环机制有关了。

1、JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。
2、一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。
3、setTimeout/setInterval等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。

事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环,之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行正在等待的任务队列,执行完一个任务队列后,如果还有任务队列,那就执行该任务队列,就这样一直循环下去,直到所有代码执行完毕。

上面代码输出结果的原因大致明白了,就是因为for循环并没有在每次循环的时候输出 i 的值,而是在循环完成之后,i 已经变为了6之后,才进行的输出。

因为事件循环机制的限制,以上代码的执行顺序是不可更改的,只要你使用了setTimeout,那就必须等到函数调用栈中可执行代码执行完毕之后,再去执行任务队列中的任务。既然代码执行顺序不可变,那可不可以把每次for循环时 i 的值保存起来,等到console.log(i)的时候再去调用保存好的 i 的值呢?这时候就需要使用闭包了。

修改后代码:

for (var i = 1; i <= 5; i++) {
	(function (i) {
		setTimeout(function timer() {
			console.log(i);
		}, i * 1000)
	})(i)
}

这里使用立即调用函数(IIFE)和匿名函数形成一个私有作用域(相当于闭包),私有作用域中的变量和全局作用域中的变量互不冲突,这种写法也叫作"命名空间";这时每次for循环传入的 i 的值都将作为私有变量被保存在内存中,等待for循环执行完毕后,跟随任务队列输出。

注:形成闭包之后,这里的i其实是两个不同的变量,for循环中的 i 为全局变量,IIFE中的 i 为私有变量。如果在全局状态下console.log(i),最后会只会输出一个6,如下代码:

for (var i = 1; i <= 5; i++) {
	(function (i) {
		setTimeout(function timer() {
			console.log(i);
		}, i * 1000)
	})(i)
}
console.log(i);  //6 (最先输出)

输出结果:
js经典面试题:setTimeout+for循环组合,使用闭包循环输出1,2,3,4,5_第2张图片
闭包也称为函数嵌套函数,将修改的代码写入setTimeout中的方式更能直观的体现闭包写法:

for (var i = 1; i <= 5; i++) {
	setTimeout(function (i) {
		return function timer(){
			console.log(i);
		}
	})(i), i * 1000)
}

在ES6(ES2015)中,因为新增了声明变量的API,所以有更简单的修改方式:将var修改为let

for(let i=1; i<=5; i++){
	setTimeout(function timer(){
		console.log(i);
	}, i * 1000)
}

let声明的变量的范围会生成一个私有作用域,也叫作块级作用域,该变量只会在当前作用域中生效,以 { } 为标识,如下代码:

{
	var a = 5;
	let b = 6;
	console.log(a); //5
	console.log(b); //6
}
console.log(a); //5
console.log(b); //b is not defined

参考资料:前端基础进阶

你可能感兴趣的:(#,JavaScript,setTimeout,IIFE,闭包,for循环,事件循环)