今天在回顾JavaScript进阶用法的时候,发现一个有趣的问题,话不多说,先上代码:
for(var j=0;j<10;j++){ setTimeout(function(){console.log(j)},5000)}
看到这三行代码,也许你会不耐烦道:又要讲闭包?要吐了好么?别急,让我们先来思考一下,这段代码在浏览器中的执行结果是什么?
执行结果显示,浏览器打印出了十个10
家都知道,JavaScript在ES6出现以前,是没有块状作用域的,这就意味着, 在for循环中用var定义的变量j,其实是属于全局的,即在全局范围内都可以被访问到,既然如此,那其实整个全局作用域中就只有一个j,每次for循环都i是在更新这个j。
那么现在关键的问题在于,为什么整个for循环会先于setTimeout执行,而不是我们正常理解的,一次迭代执行一次。
这就涉及到了JavaScript的核心特性:单线程。
JavaScript设计的初衷,是浏览器用来与用户进行交互和DOM操作的。这就决定了它必须是单线程的,设想JavaScript同事有两个线程,一个线程在DOM节点添加内容,一个线程删除该节点,浏览器就会出现混乱。所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
为了优化单线程的性能,JavaScript将任务分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有主线程中的同步任务执行完毕,异步任务才会进入执行队列执行。只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
而setTimeout,就被JavaScript定义为异步任务。每次for循环的迭代,都将setTimeout中的回调函数加入任务队列等待执行。也就是说,只有同步任务中的for循环完全结束,主线程中才会去任务队列中找到尚未执行的十个setTimeout(十次迭代)回调函数并顺序执行(先进先出)。而此时,i已经经过循环结束变成了10,所以,此时主线程执行的,是十个一摸一样的打印i的回调函数,即打印十个10。至此就完美回答了第一和第二个问题,文章开头的代码与下面的代码其实是等价的:
for(var i=0;i<10;i++){} setTimeout(console.log(i),5000) setTimeout(console.log(i),5000) setTimeout(console.log(i),5000) setTimeout(console.log(i),5000) setTimeout(console.log(i),5000) setTimeout(console.log(i),5000) setTimeout(console.log(i),5000) setTimeout(console.log(i),5000) setTimeout(console.log(i),5000) setTimeout(console.log(i),5000)