引发循环:了解JavaScript中的循环和超时

Often, JavaScript just works. And because it is written in human-readable syntax, certain things seem intuitive. But it’s easy to ignore what’s happening on a deeper level. Eventually, though, this lack of understanding results in an inability to solve a problem.

通常,JavaScript可以正常工作。 而且由于它是以人类可读的语法编写的,因此某些事情看起来很直观。 但是,很容易忽略更深层次的情况。 最终,尽管如此,缺乏理解导致无法解决问题。

Intuition is the ability to understand something immediately, without the need for conscious reasoning. — Google
直觉是无需立即进行推理就可以立即理解某些内容的能力。 - 谷歌

I spend a fair amount of time trying to solve two-dimensional problems, and a slightly larger portion of it trying to solve three-dimensional ones.

我花了大量的时间来尝试解决二维问题,而其中很大一部分要尝试解决三维问题。

While I enjoy practicing coding in my off time, by day I am an air traffic controller. The problems we face as air traffic controllers aren’t different than any other job. There are routine problems with routine solutions and unique problems with unique solutions. It’s through a deeper understanding that we can solve the unique ones.

虽然我喜欢在业余时间练习编码,但到了白天,我还是一名空中交通管制员。 作为空中交通管制员,我们面临的问题与任何其他工作都没有不同。 常规解决方案存在常规问题,独特解决方案存在独特问题。 通过更深入的了解,我们可以解决独特的问题。

From the outside looking in at air traffic control, it may seem everything is a unique problem–that there is an inherent requisite skill to do the job. However, while certain aptitudes can make learning any skill easier, it is ultimately experience that drives problem-solving to a subconscious level. The result is intuition.

从外部观察空中交通管制,似乎所有事情都是一个独特的问题-这项工作具有固有的必备技能。 但是,尽管某些才能可以使学习任何技能变得更容易,但最终的经验将解决问题的能力提升到了潜意识的水平。 结果就是直觉。

Intuition follows observation. Observe a unique problem enough times, and it and its solution become routine. It’s noticing the consistencies across each situation where we begin to develop a sense of what should happen next.

直觉跟随观察。 足够多的时间观察一个独特的问题,并且它的解决方案成为常规。 它注意到了每种情况下的一致性,在这种情况下,我们开始对下一步应该发生的事情有所了解。

Intuition does not, however, require a deep understanding. We can often point to the correct solution, without being able to articulate how or why it works. Sometimes, however, we choose solutions which seem intuitive but are in fact governed by an unfamiliar set of rules.

直觉不,但是,需要有深刻的理解。 我们经常可以指出正确的解决方案,而无法阐明其工作方式或原因。 但是,有时,我们选择看似直观但实际上受一组陌生规则约束的解决方案。

此代码输出什么? (What does this code output?)

for(var i = 1; i < 6; i++) {
  setTimeout(function() {
     console.log(i);
  },1000);
}
console.log('The loop is done!');

Take some time to think about what this code will output. We will begin to build the foundation to answer this, and we will return to this later.

花一些时间考虑一下此代码将输出的内容。 我们将开始建立基础来回答这个问题,稍后我们将返回。

JavaScript是一种语言方言。 (JavaScript is a language dialect.)

I grew up in the Northeastern United States. Although I speak English, my speech undeniably contains regional variety. This variety is called dialect. My particular dialect is an implementation (or version) of the English language standard.

我在美国东北长大。 尽管我会说英语,但我的演讲无可否认地包含地区差异。 这种变种叫做方言 。 我的特定方言是英语标准的实现 (或版本)。

It may seem standards would give birth to dialects, but it is the dialect which initially drives the need for standards. JavaScript is similar. JavaScript is the dialect, not the standard. The standard is ECMAScript, created by ECMA–the European Computer Manufacturers Association. ECMAScript is an attempt to standardize JavaScript.

似乎标准会催生方言,但最初是由方言推动对标准的需求。 JavaScript与此类似。 JavaScript是方言,不是标准。 该标准是ECMAScript ,由欧洲计算机制造商协会ECMA创建。 ECMAScript是对JavaScript进行标准化的尝试。

There is more than one implementation of ECMAScript, but JavaScript happens to be the most popular, and therefore, the name JavaScript and ECMAScript are often used interchangeably.

ECMAScript的实现不止一种,但是JavaScript恰恰是最受欢迎的,因此JavaScript和ECMAScript这个名字经常互换使用。

JavaScript在引擎中运行。 (JavaScript runs in an engine.)

JavaScript is only a text file. Like a driver without a car, it can’t go very far. Something has to run or interpret your file. This is done by a JavaScript engine.

JavaScript只是一个文本文件。 就像没有汽车的驾驶员一样,它不可能走得很远。 必须运行或解释您的文件。 这是由JavaScript引擎完成的。

A few examples of JavaScript engines include V8, the engine used by Google Chrome; SpiderMonkey, the engine used by Mozilla Firefox; and JavaScriptCore, the engine used by Apple Safari. ECMAScript, the language standard, ensures consistency across the different JavaScript engines.

JavaScript引擎的一些示例包括Google Chrome使用的V8引擎; SpiderMonkey,Mozilla Firefox使用的引擎; 和JavaScriptCore(Apple Safari使用的引擎)。 语言标准ECMAScript可确保不同JavaScript引擎之间的一致性。

JavaScript引擎在环境中运行。 (JavaScript engines run in an environment.)

While JavaScript can run in different places (for example, Node.js, a popular server-side technology, runs JavaScript and uses the same V8 engine that Google Chrome uses), the most common place to find a JavaScript engine is a web browser.

尽管JavaScript可以在不同的地方运行(例如,流行的服务器端技术Node.js,运行JavaScript并使用与Google Chrome浏览器相同的V8引擎),但查找JavaScript引擎最常见的地方是Web浏览器。

Within the browser, the JavaScript engine is just one part of a larger environment that helps bring our code to life. There are three main parts to this environment, and together they make up what is termed the runtime environment.

在浏览器中,JavaScript引擎只是大型环境的一部分,该环境有助于使我们的代码栩栩如生。 此环境由三个主要部分组成,它们共同构成了运行时环境

调用栈 (The call stack)

The first part is the location of the currently running code. This is named the call stack. There’s only one call stack in JavaScript, and this will become important as we continue to build our foundation.

第一部分是当前正在运行的代码的位置。 这被称为调用堆栈。 JavaScript中只有一个调用栈,随着我们不断建立基础,这将变得很重要。

Here is a simplified example of the call stack:

这是调用堆栈的简化示例:

function doSomething() {
   //some other code
   doSomethingElse();
   //some other code
}

function doSomethingElse() {
 //some other code
}

doSomething();

The initial call stack is empty, as there is no running code. When our JavaScript engine finally reaches the first function invocation, doSomething(), it gets added to the stack:

初始调用堆栈为空,因为没有运行代码。 当我们JavaScript引擎最终到达第一个函数调用doSomething() ,它将被添加到堆栈中:

--Call Stack--

doSomething;

Inside of doSomething() we run some other code and then reach doSomethingElse():

doSomething()内部,我们运行其他代码,然后到达doSomethingElse():

--Call Stack--

doSomething
doSomethingElse

When doSomethingElse() is done running, it is removed from the call stack:

doSomethingElse()完成运行时,将从调用堆栈中将其删除:

--Call Stack--

doSomething

Finally, doSomething() finishes the remaining code, and is also removed from the call stack:

最后, doSomething()完成剩余的代码,并从调用堆栈中将其删除:

--Call Stack--

Empty
Web API (Web APIs)

The second part of our browser environment fills somewhat of a void. Surprisingly, things such as interacting with the DOM, making server requests, and most browser-based tasks are not part of the ECMAScript language standard.

浏览器环境的第二部分填补了空白。 令人惊讶的是,诸如与DOM交互,发出服务器请求以及大多数基于浏览器的任务之类的内容都不属于ECMAScript语言标准的一部分。

Fortunately, browsers offer us added features that our JavaScript engine can plug in to. These features extend the functionality of JavaScript within the browser. They allow us to do things such as listen for events or make server requests — things that JavaScript can’t do by itself. And they are called web APIs.

幸运的是,浏览器为我们提供了JavaScript引擎可以插入的附加功能。 这些功能扩展了浏览器中JavaScript的功能。 它们使我们能够执行诸如侦听事件或发出服务器请求之类的事情,而JavaScript本身是无法做到的。 它们被称为w eb API

Many web APIs allow us to listen or wait for something to occur. When that event occurs, we then run some other code.

许多Web API允许我们监听或等待发生的事情。 当该事件发生时,我们然后运行其他代码。

Here is our call stack example expanded to include a (pretend) web API.

这是我们的调用堆栈示例,已扩展为包含(假装)Web API。

function doSomething() {
   //some other code
   listenForClick();
   doSomethingElse();
   //some other code
}

function doSomethingElse() {
 //some other code
}

listenForClick() {
   console.log('the button was clicked!')
}

doSomething();

When the browser encounters doSomething() it gets placed in the call stack:

当浏览器遇到doSomething()它将放入调用堆栈中:

--Call Stack--

doSomething

Then, it runs some other code and then encounters listenForClick(...):

然后,它运行一些其他代码,然后遇到listenForClick(...)

--Call Stack--

doSomething
listenForClick

listenForClick() gets plugged into a web API, and in this case, it is removed from our call stack.

listenForClick()插入Web API,在这种情况下,它已从我们的调用堆栈中删除。

The JavaScript engine now moves on to doSomethingElse():

JavaScript引擎现在移至doSomethingElse()

--Call Stack--

doSomething
doSomethingElse

doSomethingElse() and doSomething() finish, and the call stack is empty. But what happened to listenForClick()?

doSomethingElse()doSomething()完成,并且调用堆栈为空。 但是listenForClick()发生了什么?

事件队列 (Event Queue)

This is where we introduce the final part of our browser environment. Often, our web API code is a function that takes a callback. A callback is just some code we want to run after another function runs. For example, listening for a click event and then console.log something. In order to make sure our console.log doesn’t interfere with any currently running code, it first passes to something called an event queue.

这是我们介绍浏览器环境的最后一部分的地方。 通常,我们的Web API代码是需要回调的函数。 回调只是我们要在另一个函数运行之后运行的一些代码。 例如,先监听一下点击事件,然后监听console.log 。 为了确保我们的console.log不会干扰任何当前正在运行的代码,它首先传递给一个名为event queue事件

The event queue acts as a waiting area until our call stack is empty. Once the call stack is empty, the event queue can pass our code into the call stack to be run. Let’s continue to build upon our previous example:

在我们的调用堆栈为空之前,事件队列充当等待区域。 一旦调用堆栈为空,事件队列可以将我们的代码传递到调用堆栈中以运行。 让我们继续以前面的示例为基础:

function doSomething() {
   //some other code
   listenForClick();
   doSomethingElse();
   //some other code
}

function doSomethingElse() {
 //some other code
}

listenForClick() {
   console.log('the button was clicked!')
}

doSomething();

So now, our code runs like this:

所以现在,我们的代码运行如下:

Our engine encounters doSomething():

我们的引擎遇到doSomething()

--Call Stack--

doSomething

doSomething() runs some code and then encounters listenForClick(...). In our example, this takes a callback, which is the code we want to run after the user clicks a button. The engine passes listenForClick(…) out of the call stack and continues until it encounters doSomethingElse():

doSomething()运行一些代码,然后遇到listenForClick(...) 。 在我们的示例中,这需要一个回调,这是我们要在用户单击按钮之后运行的代码。 引擎将listenForClick(…)从调用堆栈中传递出去,并继续直到遇到doSomethingElse()为止:

--Call Stack--

doSomething
doSomethingElse

doSomethingElse() runs some code, and finishes. By this time, our user clicks the button. The web API hears the click and sends the console.log() statement to the event queue. We’ll pretend doSomething() is not done; therefore, the call stack is not empty, and the console.log() statement must wait in the event queue.

doSomethingElse()运行一些代码,并完成操作。 此时,我们的用户单击了按钮。 Web API会听到点击,并将console.log()语句发送到事件队列。 我们假装doSomething()没有完成; 因此,调用堆栈不为空,并且console.log()语句必须在事件队列中等待。

--Call Stack--

doSomething

After a few seconds, doSomething() finishes and is removed from the call stack:

几秒钟后, doSomething()完成并从调用堆栈中删除:

--Call Stack--

EMPTY

Finally, the console.log() statement can get passed into the call stack to be executed:

最后, console.log()语句可以传递到要执行的调用堆栈中:

--Call Stack--

console.log('The user clicked the button!')

Keep in mind, our code is running incredibly fast — taking single-digit milliseconds to finish. It isn’t realistic we could start our code, and our user could click a button before the code is done running. But in our simplified example, we pretend that this is true, to highlight certain concepts.

请记住,我们的代码运行速度非常快,只需几毫秒即可完成。 我们不能启动代码,并且用户可以在代码运行完成之前单击一个按钮,这是不现实的。 但是,在我们的简化示例中,我们假设这是正确的,以突出某些概念。

Together, all three parts (the call stack, the web APIs, and the event queue) form what is called the concurrency model, with the event loop managing the code that goes from the event queue into the call stack.

这三个部分(调用堆栈,Web API和事件队列)共同构成了并发模型, 事件循环管理从事件队列到调用堆栈的代码。

忽略以上示例: (Take aways from the above examples:)

JavaScript一次只能做一件事。 (JavaScript can only do one thing at a time.)

There is a misconception that people can multi-task. This isn’t true. People can, however, switch between tasks, a process called task switching.

人们可能会执行多项任务,这是一种误解。 这不是真的 但是,人们可以在任务之间切换,这个过程称为任务切换

JavaScript is similar in the sense that it can’t multitask. Because JavaScript has only one call stack, the JavaScript engine can only do one task at a time. We say this makes JavaScript single threaded. Unlike people, however, JavaScript can’t task switch without the help of our web APIs.

JavaScript在不能执行多任务的意义上相似。 因为JavaScript只有一个调用堆栈,所以JavaScript引擎一次只能执行一项任务。 我们说这使得JavaScript是单线程的 。 但是,与人们不同的是,如果没有我们的Web API的帮助,JavaScript便无法进行任务切换。

JavaScript必须先完成一项任务,然后再继续。 (JavaScript must finish a task before moving on.)

Because JavaScript can’t switch back and forth between tasks, if you have any code that takes a while to run, it will block the next line of code from running. This is called blocking code, and it happens because JavaScript is synchronous. Synchronous simply means that JavaScript must finish a task before it can start another one.

因为JavaScript不能在任务之间来回切换,所以如果您有任何代码需要花费一些时间才能运行,它将阻止下一行代码的运行。 这称为阻塞代码 ,它的发生是因为JavaScript是同步的 。 同步只是意味着JavaScript必须先完成一项任务,然后才能启动另一个任务。

An example of blocking code might be a server request which requires us to wait for data to be returned. Fortunately, the web APIs provided by the browser give us a way around this (with the use of callbacks).

阻塞代码的一个例子可能是服务器请求,它要求我们等待数据返回。 幸运的是,浏览器提供的Web API为我们提供了解决此问题的方法(使用回调)。

By moving blocking code from the call stack into the event loop, our engine can move on to the next item in the call stack. Therefore, with code running in our call stack, and code that is simultaneously running in a web API, we have asynchronous behavior.

通过将阻塞代码从调用堆栈移到事件循环中,我们的引擎可以移至调用堆栈中的下一项。 因此,通过在调用堆栈中运行的代码以及在Web API中同时运行的代码,我们具有同步行为。

Not all web APIs, however, go into the event loop. For example, console.log is a web API, but since it has no callback and doesn’t need to wait for anything, it can be executed immediately.

但是,并非所有的Web API都会进入事件循环。 例如, console.log是一个Web API,但是由于它没有回调并且不需要等待任何东西,因此可以立即执行。

Do keep in mind that single threaded is not the same as synchronous. Single threaded means “one thing at a time.” Synchronous means “finish before moving on.” Without the help of asynchronous APIs, core JavaScript is both single threaded and synchronous.

请记住,单线程与同步是不同的。 单线程意味着“一次一件事情”。 同步意味着“继续前进之前完成”。 没有异步API的帮助,核心JavaScript既是单线程的又是同步的。

范围的独家新闻 (The scoop on scope)

Before we return to our original question, we need to touch on scope. Scope is the term used to describe which parts of our code have access to which variables.

在回到原始问题之前,我们需要谈一下范围。 范围是用于描述我们代码的哪些部分可以访问哪些变量的术语。

Intuitively, it may seem that a variable declared and initialized by a for loop would only be available within that for loop. In other words, if you tried to access it outside of the loop, you would get an error.

直观地讲,似乎由for loop声明和初始化的变量仅在for loop内可用。 换句话说,如果您尝试在循环之外访问它,则会收到错误消息。

This isn’t the case. Declaring a variable with the var keyword creates a variable that is also available in its parent scope.

事实并非如此。 用var声明一个变量 关键字创建一个变量,该变量在其父作用域中也可用。

This example shows that a variable declared by var within a for loop is also available within the parent scope (in this case, the global scope).

此示例显示,在父for loop (在本例中为全局范围)内,在for loopvar声明的变量也可用。

for(var a = 1; a < 10; a++) {} // declared "inside" the loop
console.log(a); // prints "10" and is called "outside the loop"

答案揭晓 (The answer revealed)

At this point, we’ve discussed enough to build our answer.

至此,我们已经进行了足够的讨论来建立答案。

Here is our example revisited:

下面是我们的示例:

for(var i = 1; i < 6; i++) {
  setTimeout(function() {
     console.log(i);
  },1000);
}
console.log('The loop is done!');

Intuitively, you might believe this will print the numbers one through five, with one second between each number being printed:

直观地,您可能会相信这将打印出一到五的数字,并且每个数字之间打印一秒钟:

// one second between each log

1
2
3
4
5
The loop is done!

However, what we actually output is:

但是,我们实际输出的是:

The loop is done!

// then about one second later and all at once

6
6
6
6
6
发生了什么? (What’s happening?)

Recall our discussion about web APIs. Asynchronous web API’s, or those with callbacks, go through the event loop. setTimeout()happens to be an asynchronous web API.

回想一下我们有关Web API的讨论。 异步Web API或具有回调的Web API会经历事件循环。 setTimeout()恰好是异步Web API。

Every time we loop, setTimeout() is passed outside of the call stack and enters the event loop. Because of this, the engine is able to move to the next piece of code. The next piece of code happens to be the remaining iterations of the loop, followed by console.log(‘The loop is done!’).

每次循环时, setTimeout()都会在调用堆栈之外传递并进入事件循环。 因此,引擎能够移至下一段代码。 下一段代码恰好是循环的其余迭代,其次是console.log('The loop is done!')

To show the setTimeout() statements are being passed from the call stack, and the loop is running, we can place a console.log() statement outside of the setTimeout() function and print the results. We can also place a built-in timer method to show just how quickly everything is happening. We use console.time() and console.timeEnd() to do this .

为了显示从调用堆栈传递了setTimeout()语句,并且循环正在运行,我们可以在setTimeout()函数外部放置一个console.log()语句,并打印结果。 我们还可以放置一个内置的计时器方法来显示一切进行的速度。 我们使用console.time()console.timeEnd()来做到这一点。

console.time('myTimer');
for(var i = 1; i < 6; i++) {
   console.log('Loop Number' + i); // added this
   setTimeout(()=>{
      console.log(i);
   },1000);
}
console.log('The loop is done!');
console.timeEnd('myTimer');

Results:

结果:

Loop Number 1
Loop Number 2
Loop Number 3
Loop Number 4
Loop Number 5
The loop is done!

// then, about one second later and all at once:

6
6
6
6
6
myTimer: 1.91577ms   // Wow, that is quick!

First, we can see the loop is in fact running. In addition, the timer we added tells us that everything other than our setTimeout() functions took less than two milliseconds to run! That means each setTimeout() function has about 998 milliseconds remaining before the code it contains goes into the event queue and then finally into the call stack. Remember earlier when I said it would be difficult for a user to be faster than our code!

首先,我们可以看到循环实际上正在运行。 另外,我们添加的计时器告诉我们,除了setTimeout()函数之外的所有其他东西都花费不到两毫秒的时间来运行! 这意味着每个setTimeout()函数在其包含的代码进入事件队列,然后最终进入调用堆栈之前,大约还有998毫秒的剩余时间。 记得早些时候,我曾说过用户很难比我们的代码快!

If you run this code multiple times, you will likely notice the timer output will change slightly. This is because your computer’s available resources are always changing and it might be slightly faster or slower each time.

如果您多次运行此代码,您可能会注意到计时器输出将略有变化。 这是因为计算机的可用资源总是在变化,并且每次可能会稍快或稍慢。

So here is what’s happening:

所以这是正在发生的事情:

  1. Our engine comes across our for loop. We declare and initialize a global variable named i equal to one.

    我们的引擎遇到了我们的for循环。 我们声明并初始化一个名为i等于1的全局变量。

  2. Each iteration of loop passes setTimeout() to a web API and into the event loop. Therefore, our for loop finishes very quickly, since there is no other code inside of it to run. In fact, the only thing our loop does is change the value of i to six.

    循环的每次迭代都将setTimeout()传递到Web API并进入事件循环。 因此,我们的for loop很快完成,因为其中没有其他代码可以运行。 实际上,循环唯一要做的就是将i的值更改为6。

  3. At this point, the loop is over, our setTimeout() functions are still counting down, and all that remains in the call stack is console.log(‘The loop is done!’).

    至此,循环结束了,我们的setTimeout()函数仍在递减计数,并且调用堆栈中剩下的全部是console.log('The loop is done!')

  4. Fast forward a bit, and the setTimeout() functions have finished, and the console.log(i) statements go into the event queue. By this time, our console.log(‘The loop is done!’) has been printed and the call stack is empty.

    快进一点, setTimeout()函数已经完成, console.log(i)语句进入了事件队列。 此时,我们的console.log('The loop is done!')已打印,并且调用堆栈为空。

  5. Since the call stack is empty, the fiveconsole.log(i) statements get passed from the event queue into the call stack.

    由于调用堆栈为空,因此五个console.log(i)语句从事件队列传递到调用堆栈。

  6. Remember, i is now equal to six, and that’s why we see five sixes printed to the screen.

    记住, i现在等于六个,这就是为什么我们在屏幕上看到五个六个的原因。

让我们创建我们认为会得到的输出 (Let’s create the output we thought we would get)

Up to this point, we’ve discussed the actual output of a few simple lines of code that turned out to be not-so-simple. We’ve talked about what’s happening on a deeper level and what the result is. But, what if we want to create the output we thought we would get? In other words, how can we reverse engineer the following results:

到现在为止,我们已经讨论了几行简单的代码的实际输出,但事实并非如此。 我们已经讨论了更深层次的情况以及结果。 但是,如果我们想创建我们认为会得到的输出,该怎么办? 换句话说,我们如何对以下结果进行逆向工程:

1 // after one second, then
2 // one second later (2 seconds total)
3 // one second later (3 seconds total)
4 // one second later (4 seconds total)
5 // one second later (5 seconds total)
'The loop is done!' // one second later (6 seconds total)
超时时间是否有变化? (Does the duration on our timeout change anything?)

Setting the timeout’s duration to zero seems like a possible solution. Let’s give it a try.

将超时时间设置为零似乎是一种可能的解决方案。 试一试吧。

for(var i = 1; i < 6; i++) {
   setTimeout(()=>{
      console.log(i);
   },0);
}
console.log('The loop is done!');

Results:

结果:

// Everything appears (essentially) at once

The loop is done!
6
6
6
6
6

It still didn’t work. What happened?

仍然没有用。 发生了什么?

Remember, just because the duration of setTimeout() is zero, it is still asynchronous and handled by a web API. Regardless of the duration, it will be passed to the event queue and then the call stack. So even with a timeout of zero, the process remains the same, and the output is relatively unchanged.

请记住,仅仅因为setTimeout()的持续时间为零,它仍然是异步的并由Web API处理。 无论持续时间如何,它都会传递给事件队列,然后传递给调用堆栈。 因此,即使超时为零,该过程也保持不变,并且输出相对不变。

Notice I said relatively. One thing you may have noticed that was different, was everything printed almost at once. This is because the duration of setTimeout() expires instantly, and its code gets from the web API, into the event queue, and finally into the call stack almost immediately. In our previous example, our code had to wait 1000 milliseconds before it went into the event queue and then the call stack.

注意我说的相对 。 您可能已经注意到的一件事与众不同, 几乎所有内容都立即打印出来。 这是因为setTimeout()的持续时间立即终止,并且其代码从Web API进入事件队列,最后几乎立即进入调用堆栈。 在前面的示例中,我们的代码必须等待1000毫秒才能进入事件队列,然后进入调用堆栈。

So, if changing the duration to zero didn’t work, now what?

那么,如果将持续时间更改为零不起作用,那又如何呢?

考察范围 (Revisiting Scope)

What will this code output?

此代码将输出什么?

function myFunction1() {
   var a = 'Brandon';
   console.log(a);
}
function myFunction2() {
   var a = 'Matt';
   console.log(a);
}
function myFunction3() {
   var a = 'Bill';
   console.log(a);
}
myFunction1()
myFunction2()
myFunction3()

Notice how each function uses the same variable named a. It would seem each function might throw an error, or possibly overwrite the value of a.

请注意,每个函数如何使用名为a的相同变量。 这似乎是每一个功能可能会引发错误,或可能覆盖的值a

Results:

结果:

Brandon
Bill
Matt

There is no error, and a is unique each time.

有没有误差, a是独特的每一次。

It appears the variable a is unique to each function. It’s very similar to how an address works. Street names and numbers are invariably shared across the world. There is more than a single 123 Main St. It’s the city and state which provide scope to which address belongs where.

看来变量a对于每个函数都是唯一的。 这与地址的工作方式非常相似。 街道名称和号码在世界各地始终共享。 有一个以上的Main Main123。它是城市和州,它提供了地址所属的范围

Functions work in the same way. Functions act as a protective bubble. Anything inside of that bubble can’t be accessed by anything outside. This is why the variable a is not actually the same variable. It’s three different variables located in three different places in memory. They just so happen to all share the same name.

功能以相同的方式工作。 功能起到保护气泡的作用。 气泡内的任何内容都不能被外界访问。 这就是变量a实际上不是相同变量的原因。 它是位于内存中三个不同位置的三个不同变量。 他们恰好都共享相同的名字。

将范围原则应用于我们的示例: (Applying the principles of scope to our example:)

We know we have access to the iterative value of i, just not when the setTimeout() statements finish. What if we take the value of i and package it with the setTimeout() statement in its own bubble (as a way to preserve i)?

我们知道我们可以访问i的迭代值,而不能在setTimeout()语句完成时访问。 如果我们将i的值并用setTimeout()语句包装在其自身的气泡中(作为保存i一种方式)怎么办?

for(var i = 1; i < 6; i++) {
   function timer(){ // create a unique function (scope) each time
      var k = i; // save i to the variable k which
      setTimeout(()=>{
         console.log(k);
      },1000);
   }
   timer();
}

Result:

结果:

The loop is done!
1
2
3
4
5
几乎可以工作。 我们做了什么? (It almost works. What did we do?)

We are beginning to get into the topic of closures. A deep discussion on closures goes beyond the scope of this article. However, a brief introduction will help our understanding.

我们开始进入闭包的话题 关于闭包的深入讨论超出了本文的范围。 但是,简要介绍将有助于我们的理解。

Remember, each function creates a unique scope. Because of this, variables with the same name can exist in separate functions and not interfere with each other. In our most recent example, each iteration created a new and unique scope (along with a new and unique variable k). When the for loop is done, these five unique values of k are still in memory and are accessed appropriately by our console.log(k) statements. That is closure in a nutshell.

请记住,每个函数都会创建一个唯一的作用域。 因此,具有相同名称的变量可以存在于单独的函数中,并且不会互相干扰。 在我们最近的示例中,每次迭代都创建了一个新的唯一范围(以及新的唯一变量k )。 当for loop完成时, k这五个唯一值仍在内存中,并由我们的console.log(k)语句适当地访问。 简而言之,就是关闭。

In our original example where we declare i with var, each iteration overwrote the value of i (which in our case was a global variable).

在我们用var声明i原始示例中,每次迭代都覆盖了i的值(在我们的例子中是全局变量)。

ES6使这一过程更加清洁。 (ES6 makes this much cleaner.)

In 2015, ECMAScript released a major update to its standards. The update contained many new features. One of those features was a new way to declare variables. Up to this point, we have used the var keyword to declare variables. ES6 introduced the let keyword.

2015年,ECMAScript对标准进行了重大更新。 此更新包含许多新功能。 这些功能之一是声明变量的新方法。 到目前为止,我们已经使用var关键字声明变量。 ES6引入了let关键字。

for(let i = 1; i < 6; i++) {
   setTimeout(()=>{
      console.log(i);
   },1000);
}
console.log('The loop is done!');

Results:

结果:

The loop is done!
1
2
3
4
5

Just by changing var to let, we are much closer to the result we want.

只需将var更改为let ,我们就可以更接近所需的结果。

“ let”和“ var”简介 (A brief introduction to “let” vs “var”)

In our example, let does two things:

在我们的示例中, let做两件事:

First, it makes i available only inside our for loop. If we try to log i outside of the loop, we get an error. This is because let is a block scope variable. If it is inside a block of code (such as a for loop) it can only be accessed there. var is function scoped.

首先,它使i仅在我们的for循环内可用。 如果我们尝试将i记录在循环之外,则会出现错误。 这是因为let是一个块范围变量。 如果它在代码块内(例如for loop ),则只能在其中进行访问。 var是函数范围的。

An example to show let vs var behavior:

显示let vs var行为的示例:

function variableDemo() {
   var i = 'Hello World!';
   for(let i = 1; i < 3; i++) {
      console.log(i); // 1, 2, 3
   }
   console.log(i); // "Hello World!" 
   // the for-loop value of i is hidden outside of the loop with let
}

variableDemo();
console.log(i); //Error, can't access either value of i

Notice how we don’t have access to either i outside of the function variableDemo(). This is because ‘Hello World’ is function scoped, and i is block scoped.

注意,我们如何无法访问variableDemo()函数之外的任何一个i 。 这是因为'Hello World'是函数范围的,而i是块范围的。

The second thing let does for us is create a unique value of i each time the loop iterates. When our loop is over, we have created six separate values of i that are stored in memory that our console.log(i) statements can access. With var, we only had one variable that we kept overwriting.

let做的第二件事是每次循环迭代时都创建一个唯一的i值。 循环结束后,我们创建了六个独立的i值,这些值存储在console.log(i)语句可以访问的内存中。 使用var,我们只有一个变量会被覆盖。

循环未完成。 (The loop is not done.)

We’re almost there. We still are logging 'The loop is done!' first, and we aren’t logging everything one second apart. First, we will look at two ways to address the The loop is done! output.

我们快到了。 我们仍然在记录'The loop is done!' 首先,我们并没有将所有记录间隔一秒钟。 首先,我们将探讨两种解决The loop is done! 输出。

选项1:使用setTimeout()和并发模型使我们受益。 (Option 1: Using setTimeout() and the concurrency model to our advantage.)

This is fairly straightforward. We want The loop is done! to pass through the same process as the console.log(i) statements. If we wrap The loop is done! in a setTimeout() whose duration is greater to or equal than the for loop timeouts, we ensure The loop is done! arrives behind and expires after the last for loop timeouts.

这很简单。 我们希望The loop is done! 通过与console.log(i)语句相同的过程。 如果我们包装The loop is done! 在持续时间大于或等于for loop超时的setTimeout() ,我们确保The loop is done! 在最后一个for loop超时之后到达并过期。

We’ll break up our code a bit to make it a bit clearer:

我们将对代码进行一些分解以使其更加清晰:

function loopDone() { // we will call this below
   console.log('The loop is done!)'
}
               
for(let i = 1; i < 6; i++) {
   setTimeout(()=>{
      console.log(i);
   },1000);
}
   
setTimeout(loopDone, 1001);

Results:

结果:

1
2
3
4
5
The loop is done!
选项2:检查最终的console.log(i)完成情况 (Option 2: Check for the final console.log(i) completion)

Another option is to check when the console.log(i) statements are done.

另一个选择是检查console.log(i)语句何时完成。

function loopDone() {
   console.log('The loop is done!');
}
for(let i = 1; i < 6; i++) {
   setTimeout(()=>{
      console.log(i);
      if(i === 5){ // check when the last statement has been logged
         loopDone();
      }
   },1000);
}

Results:

结果:

1
2
3
4
5
The loop is done!

Notice that we placed our loop completion check within the setTimeout() function, not within the main body of the for loop.

请注意,我们将循环完成检查放在setTimeout()函数中, 而不是在for循环的主体中。

Checking when the loop is done won’t help us, since we still must wait for the timeouts to complete. What we want to do is check when the console.log(i) statements are done. We know this will be after the value of i is 5 and after we’ve logged it. If we place our loop completion check after the console.log(i) statement, we can ensure we’ve logged the final i before we run loopDone().

检查循环何时完成对我们没有帮助,因为我们仍然必须等待超时完成。 我们要做的是检查console.log(i)语句何时完成。 我们知道这将 i值为5 之后并记录下来。 如果将循环完成检查放在console.log(i)语句之后,则可以确保运行loopDone() 之前已经记录了最后一个i

让一切发生在一秒钟之内。 (Getting everything to happen one second apart.)

Everything is happening essentially at the same time because the loop is so fast, and all the timeouts arrive at the web API within milliseconds of each other. Therefore, they expire around the same time and go to the event queue and call stack around the same time.

由于循环如此之快,所有事情基本上都在同一时间发生,并且所有超时都在毫秒之间到达Web API。 因此,它们大约在同一时间到期,并同时到达事件队列和调用堆栈。

We can’t easily change when they arrive at the web API. But we can, with the unique value of each i, delay how long they stay there.

当它们到达Web API时,我们不能轻易更改。 但是,通过每个i的唯一值,我们可以将它们停留在那里的时间延迟。

function loopDone() {
   console.log('The loop is done!');
}
for(let i = 1; i < 6; i++) {
   setTimeout(()=>{
      console.log(i);
      if(i === 5){ 
         loopDone();
      }
   },i * 1000); // multiple i by 1000
}

Since i is now unique (because we are using let), if we multiply i by 1000, each timeout will last one second longer than the previous timeout. The first timeout will arrive with a 1000 millisecond duration, the second with 2000 and so forth.

由于i现在是唯一的(因为我们使用let ),因此如果将i乘以1000,则每个超时将比前一个超时持续一秒。 第一个超时时间为1000毫秒,第二个超时时间为2000,依此类推。

Although they arrive at the same time, it will now take each timeout one second longer than the previous to pass to the event queue. Since our call stack is empty by this point, it goes from the event queue immediately into the call stack to be executed. With each console.log(i) statement arriving one second apart in the event queue, we will almost have our desired output.

尽管它们是在同一时间到达的,但现在每个超时要比上一个超时长一秒,才能传递到事件队列。 由于此时我们的调用堆栈为空,因此它将立即从事件队列进入要执行的调用堆栈。 随着每个console.log(i)语句在事件队列中相距一秒钟到达,我们几乎将获得所需的输出。

1 // after one second, then
2 // one second later (2 seconds total)
3 // one second later (3 seconds total)
4 // one second later (4 seconds total)
5 // one second later (5 seconds total)
'The loop is done!' // still occurs with the final log

Notice that The loop is done! is still arriving with the last console.log(i) statement, not one second after it. This is because when i===5 loopDone() is run. This prints both the i and The loop is done! statements around the same time.

请注意, The loop is done!通过最后一个console.log(i)语句到达,而不是一秒钟之后到达。 这是因为当i===5 loopDone()运行时。 这将同时打印iThe loop is done! 大约在同一时间发表声明。

We can simply wrap loopDone() in a setTimeout() to address this.

我们可以简单地将loopDone()包装在setTimeout()以解决此问题。

function loopDone() {
   console.log('The loop is done!');
}
for(let i = 1; i < 6; i++) {
   setTimeout(()=>{
      console.log(i);
      if(i === 5){ 
         setTimeout(loopDone, 1000); // update this
      }
   },i * 1000);
}

Results:

结果:

1 // after one second, then
2 // one second later (2 seconds total)
3 // one second later (3 seconds total)
4 // one second later (4 seconds total)
5 // one second later (5 seconds total)
'The loop is done!' // one second later (6 seconds total)

We finally have the results we wanted!

我们终于有了想要的结果!

Most of this article stemmed from my own struggles and the subsequent aha! moments in an attempt to understand closures and the JavaScript event loop. I hope this can make sense of the basic processes at play and serve as a foundation for more advanced discussions of the topic.

本文的大部分内容来自于我自己的奋斗以及随后的啊哈! 试图理解闭包和JavaScript事件循环的时刻。 我希望这可以理解正在发挥作用的基本过程,并为对该主题进行更高级的讨论奠定基础。

Thanks!

谢谢!

woz

沃兹

翻译自: https://www.freecodecamp.org/news/thrown-for-a-loop-understanding-for-loops-and-timeouts-in-javascript-558d8255d8a4/

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值