有道经典面试题:
for(var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
},100);
}输出结果是5个5。以下根据自己的见解以及借鉴别人的分析思路,总结一下。
JS是单线程的,在程序执行时,所走的程序路径是按着顺序排下来的,前一个程序执行完后一个程序才能执行,如果前一个程序耗时很久,后一个程序就不得不一直等着。
由于这种方式很浪费资源,为了解决这一问题,开发者将任务分为两类:一种是同步任务,另一种是异步任务。同步任务指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是不进入主线程、而进入任务队列的任务,只要主线程空了,任务队列中的任务就开始执行。
浏览器为了能处理一些异步任务,引入了事件循环机制。其中,微任务一般是由JS自身创建,比如 Promise ,MutationObserver ,Object.observe ·,process.nextTick 等;宏任务一般是由浏览器发起的,比如script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)。任务执行的优先级为:JS所有主线程的同步任务先执行,然后执行微任务队列的程序,最后执行宏任务队列,秉承先进先出的原则。
接下来分析面试题,for循环括号内是同步代码,而定时器是异步任务,每次循环定时器就会进入任务队列等待主线程的任务执行完,也就等同于如下代码:
for(var i = 0; i < 5; i++) {
}
setTimeout(function () {
console.log(i);
},100);
setTimeout(function () {
console.log(i);
},100);
setTimeout(function () {
console.log(i);
},100);
setTimeout(function () {
console.log(i);
},100);
setTimeout(function () {
console.log(i);
},100);for循环里定义的i变量其实暴露在全局作用域内,于是5个定时器里的匿名函数它们其实共享了同一个作用域里的同一个变量。主线程执行完后i的值就变成了5,执行任务队列中的任务也就是执行五次定时器内的代码,最后也就输出5个5。
如果想让程序最终输出0,1,2,3,4,有三种解决方法:
1、立即执行函数
for(var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function () {
console.log(i);
});
},100)(i)
}立即执行函数生成了闭包的效果,新建了一个作用域,这个作用域接收到每次循环的i值保存了下来,即使循环结束,闭包形成的作用域也不会被销毁。
2、let关键字
for(let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
},100);
}let关键字劫持了for循环的块作用域,产生了类似闭包的效果。并且在for循环中使用let来定义循环变量还会有一个特殊效果:每一次循环都会重新声明变量i,随后的每个循环都会使用上一个循环结束时的值来初始化这个变量i。
3、使用try...catch语句
for(var i = 0; i < 5; i++) {
try {
throw(i)
} catch(j) {
setTimeout(function () {
console.log(j);
});
}
}try...catch语句的catch后面的花括号是一个块作用域,和let的效果一样。所以在try语句块里抛出循环变量i,然后在catch的块作用域里接收到传过来的i,就可以将循环变量保存下来,实现类似闭包和let的效果。
















