译者:嘴里起了个泡
原文地址: wsvincent.com/javascript-…javascript
这篇文章详细介绍了JS在执行for循环里面的 setTimeout()
语句的时候发什么了什么。这是面试中常常会被问到的一个问题,由于这个问题的答案涉及到了几个JS的核心知识点:闭包(closures),提高(hoisting)和事件循环(the event loop)。java
For
循环是JS开发中常用的。它会一直运行直到其中的判断条件为false。一个For
循环包含三个分句:一个初始化表达式,一个条件表达式和一个更新表达式。面试
for (var i = 1; i < 5; i++) {
console.log(i); // 1 2 3 4
}
复制代码
如今咱们的三个分句以下:浏览器
var i = 1
i < 5
i++
须要注意的是在这个for
循环结束的时候,变量i
的值其实是5,不是4。咱们从初始化开始,每次i
递增1,而后检查i
是否知足条件。换句话说,咱们会按照1,3,2的顺序执行这三个分句,尽管逻辑上会认为它们应该按顺序执行。
让咱们来检查一下for
循环里实际发生了什么:bash
i
值为1,增长到2,检查2 < 5?知足条件,因此打印输出。i
值为2,增长到3,检查3 < 5?知足条件,因此打印输出。i
值为3,增长到4,检查4 < 5?知足条件,因此打印输出。i
值为4,增长到5,检查5 < 5?不知足条件,终止循环。i
最终等于5,可是却只打印出来了1-4。咱们能够经过下面代码来证实这点。for (var i = 1; i < 5; i++) {
console.log(i); // 1 2 3 4
}
console.log("The value of i is now: ", i); // "The value of i is now: 5"
复制代码
关键字Var
的做用域是函数范围内,意味着它位于一个封闭的函数中。但咱们上面的例子中并无函数,因此它的做用域就是全局。也就是说,上面的for
循环建立了一个全局变量i
。
请注意,既然var
的做用域是函数范围内,那么i
的做用域就会被设置到离它最近的函数中。在这个例子中,它将会是全局变量。闭包
若是咱们想在循环中每秒输出一次应该怎么作呢?咱们会想固然的认为只要添加一个setTimeout
方法就能达到这个效果。并发
for (var i = 1; i < 5; i++) {
setTimeout(() => console.log(i), 1000) // 5 5 5 5
}
复制代码
事与愿违!!!为何没有输出1 2 3 4
呢?在这个微妙的例子中实际上发生了不少事情。
简单的回答就是for
循环先执行掉了,而后再去寻找i
的值,发现是5,而后把它打印了四次,每一个循环打印一次。
即便咱们把循环的时间间隔设置成0,结果仍是同样的。异步
for (var i = 1; i < 5; i++) {
setTimeout(() => console.log(i), 0) // 5 5 5 5
}
复制代码
我相信你对此确定很疑惑。不用担忧:你很快就会知道这究竟是怎么回事了。函数
JavaScript是单线程单一并发语言,这意味着它一次只能处理一个任务或一段代码。让咱们接着看: 因此咱们如何用它写出异步的代码呢,就好比上面例子中的setTimeout()
? 答案是JavaScript运行在浏览器中,浏览器作了不少事情不只仅是执行代码这么简单。事实上,浏览器须要考虑这四个部分:oop
DOM
,setTimeout
等等下面这个图片来自Philip Roberts’s fantastic talk on the Event Loop视频里的截图:
运行引擎执行咱们的代码,每一个浏览器都有一个稍微不一样的引擎。例如,Chrome使用V8引擎,这也刚好为NodeJs提供支持。该引擎一次只能执行一段代码。
Web APIs是浏览器提供给咱们的,其中包含了像setTimeout()
这种方法。若是你在浏览器的控制台把window
打印出来,你会看到一个很长很长的默认的API列表。
for
循环和
setTimeout
的例子中是如何一块儿运行工做的。
setTimeout
能够经过闭包拿到i
的值。咱们把i
放在console.log
语句里,可是i
的值却被设置在外面一层的封闭范围内,即for
循环里。既然内部函数能够拿到外部函数的变量,咱们就能去for
循环里取到i
的值,即5。