关于作用域和异步的一道题

修改下列代码,使得打印结果为0,1,2,3,4

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

打印结果为5个5.
我的理解:

//分解
var i = 0;
setTimeout(function() {console.log(i);}, 1000);		//进入任务队列
i++;
setTimeout(function() {console.log(i);}, 1000);		//进入任务队列
i++;
setTimeout(function() {console.log(i);}, 1000);		//进入任务队列
i++;
setTimeout(function() {console.log(i);}, 1000);		//进入任务队列
i++;
setTimeout(function() {console.log(i);}, 1000);		//进入任务队列

loupe上查看事件循环过程
image

之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值:5。在之后执行超时逻辑时,所有的 i 都是同一个变量,因而输出的都是同一个最终值。
也就是说上面的执行顺序实际相当是这样的:

//执行栈,执行同步代码
var i = 0;
setTimeout();      //停1s后将回调函数送入任务队列
i++;
setTimeout();      //停1s后将回调函数送入任务队列
i++;
setTimeout();      //停1s后将回调函数送入任务队列
i++;
setTimeout();      //停1s后将回调函数送入任务队列
i++;
setTimeout();      //停1s后将回调函数送入任务队列
//任务队列,执行异步代码
function() {console.log(i)};
function() {console.log(i)};
function() {console.log(i)};
function() {console.log(i)};
function() {console.log(i)};

JS的解析是由浏览器中的JS解析引擎完成的。JS是单线程运行,也就是说,在同一个时间内只能做一件事,所有的任务都需要排队,前一个任务结束,后一个任务才能开始。但是又存在某些任务比较耗时,如IO读写等,所以需要一种机制可以先执行排在后面的任务,这就是:同步任务(synchronous)和异步任务(asynchronous)。
​ JS的执行机制就可以看做是一个主线程加上一个任务队列(task queue)。同步任务就是放在主线程上执行的任务,异步任务是放在任务队列中的任务。所有的同步任务在主线程上执行,形成一个执行栈;异步任务有了运行结果就会在任务队列中放置一个事件;脚本运行时先依次运行执行栈,然后会从任务队列里提取事件,运行任务队列中的任务,这个过程是不断重复的,所以又叫做事件循环(Event loop)。
image

方法一:利用let
在使用 let 声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。每个 setTimeout 引用的都是不同的变量实例,所以 console.log 输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。

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

let声明i是一个局部变量,i与异步回调函数绑定了,如下:

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

方法二:IIFE---用立即调用的函数表达式来构造块作用域

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

使用 IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。

输出5个5和输出0,1,2,3,4的原因在于回调函数是否绑定了它进入任务队列时的i。
感谢阅读!

参考:

  1. 从输入URL到页面展示,你想知道些什么?
  2. JavaScript同步、异步、回调执行顺序分析
  3. JavaScript中的Event Loop(事件循环)机制
  4. 《JavaScript高级程序设计(第4版)》3.3.2 for循环中的let声明
原文地址:https://www.cnblogs.com/liulangbxc/p/14799672.html