[译]JS闭包:For循环中的setTimeout 谁践踏了优雅 2022-01-20 02:51 199阅读 0赞 > 译者:嘴里起了个泡 > 原文地址: [wsvincent.com/javascript-…][wsvincent.com_javascript-] 这篇文章详细介绍了JS在执行for循环里面的 `setTimeout()` 语句的时候发什么了什么。这是面试中经常会被问到的一个问题,因为这个问题的答案涉及到了几个JS的核心知识点:[闭包(closures)][closures],[提升(hoisting)][hoisting]和[事件循环(the event loop)][the event loop]。 ## For循环 ## `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`循环里实际发生了什么: * 第一步:`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 ## 如果我们想在循环中每秒输出一次应该怎么做呢?我们会想当然的认为只要添加一个`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 运行引擎 ## JavaScript是单线程单一并发语言,这意味着它一次只能处理一个任务或一段代码。让我们接着看: 所以我们如何用它写出异步的代码呢,就比如上面例子中的`setTimeout()`? 答案是JavaScript运行在浏览器中,浏览器做了很多事情不仅仅是执行代码这么简单。事实上,浏览器需要考虑这四个部分: * JavaScript运行时引擎 * 浏览器提供的Web APIs,比如`DOM`,`setTimeout`等等 * 具有回调函数(如onClick和onLoad)的事件的回调队列 * 事件循环 下面这个图片来自[Philip Roberts’s fantastic talk on the Event Loop][Philip Roberts_s fantastic talk on the Event Loop]视频里的截图: **运行引擎**执行我们的代码,每个浏览器都有一个稍微不同的引擎。例如,Chrome使用[V8][]引擎,这也恰好为NodeJs提供支持。该引擎一次只能执行一段代码。 **Web APIs**是浏览器提供给我们的,其中包含了像`setTimeout()`这种方法。如果你在浏览器的控制台把`window`打印出来,你会看到一个很长很长的默认的API列表。 这些API是由浏览器在一个单独的进程里独立运行的。 **这就是JavaScript可以发生异步的原因!!!** 并不是JavaScript本身可以同时做多件事;而是,浏览器可以同时为我们运行多个不同的进程。 到这里我希望你提出的问题是,运行引擎和Web API 是怎么样相互协同工作的?答案是通过 **回调队列**和 **事件循环**。 **回调队列**它是需要在JavaScript运行引擎中断后执行的一个任务队列。 **事件循环**是最后一个需要破解的谜团,它是一个不断运行的循环,用来连接堆栈和回调队列。 接下来让我们看一下它们在我们 `for`循环和 `setTimeout`的例子中是如何一起运行工作的。 ## 闭包 ## `setTimeout`可以通过闭包拿到`i`的值。我们把`i`放在`console.log`语句里,但是`i`的值却被设置在外面一层的封闭范围内,即`for`循环里。既然内部函数可以拿到外部函数的变量,我们就能去`for`循环里取到`i`的值,即5。 [wsvincent.com_javascript-]: https://link.juejin.im?target=https%3A%2F%2Fwsvincent.com%2Fjavascript-closure-settimeout-for-loop%2F [closures]: https://link.juejin.im?target=https%3A%2F%2Fwsvincent.com%2Fjavascript-scope-closures%2F [hoisting]: https://link.juejin.im?target=https%3A%2F%2Fwsvincent.com%2Fjavascript-hoisting%2F [the event loop]: https://link.juejin.im?target=https%3A%2F%2Fwsvincent.com%2Fjavascript-event-loop%2F [Philip Roberts_s fantastic talk on the Event Loop]: https://link.juejin.im?target=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D8aGhZQkoFbQ%26amp%3Bfeature%3Dyoutu.be [V8]: https://link.juejin.im?target=https%3A%2F%2Fv8.dev%2F
还没有评论,来说两句吧...