2

I have been looking at the anwers already posted on StackOverflow and none of them are satisfactory. They all give a practical solution to the problem, which doesn't solve my issue.

So take a look at this example:

for(var i = 1; i <= 5; i++) { 
  setTimeout(function(){ 
    console.log(i) 
    }, 1000); 
}

This is the classic example, which always yields:

6
6
6
6
6

This is all fine and dandy. The way I understand this is that the for loop runs and sets 5 timeouts to run after 1 second. When 1 second has passed the function inside the setTimeout will run. Since this is 1 second, i will long before that have gotten the value 6.

So I tried something else. What if I set the timeout to equal 0ms? Will that change the outcome?


for(var i = 1; i <= 5; i++) { 
  setTimeout(function(){ 
    console.log(i) 
    }, 0); 
}

No it doesn't.

6
6
6
6
6

This makes no sense. Why would the asyncronous setTimeout function run only after the for loop is done. I even tried setting the for loop value i <= 50000 and still the same outcome. This makes no sense whatsoever. In my head, once 1ms has passed, the setTimeout calls the function regardless if it's inside the for loop or not. Can someone please explain this to me? Thanks, i've read countless medium, stackoverflow and blog articles, but it still doesn't click. The scope explanation also doesn't help.

lukact
  • 50
  • 11
  • var i gets hoisted and the for loop is not scoped. The value is 6 when the for loop finishes – user120242 Jun 25 '20 at 11:40
  • `i` is bound to the function scope. By the time the timeout gets called is is `6`. Use `let` or bind it to the timeout. – Lain Jun 25 '20 at 11:40
  • `setTimeout` will schedule something for *later* and will not execute stuff now. Also, there is a minimum timeout of 4ms with `setTimeout` – VLAZ Jun 25 '20 at 11:40
  • @Lain thanks for the answer. I know let works and solves the problem, that doesn't help me though, but thanks anyhow :) – lukact Jun 25 '20 at 11:40
  • Does this answer your question? [Declaring var inside Javascript for loop declaration](https://stackoverflow.com/questions/10200994/declaring-var-inside-javascript-for-loop-declaration) – user120242 Jun 25 '20 at 11:41
  • @VLAZ So if I set the timeout to 40ms and set i <= 500000, the timeouts will run before the foor loop is finished? – lukact Jun 25 '20 at 11:41
  • Relevant: [Why is setTimeout(fn, 0) sometimes useful?](https://stackoverflow.com/q/779379) – VLAZ Jun 25 '20 at 11:42
  • setTimeout will defer the execution in the event loop and place it in the queue. The for loop must finish before it gets there – user120242 Jun 25 '20 at 11:42
  • *"Why would the asyncronous setTimeout function run only after the for loop is done."* Because that's how scheduling works in JavaScript. The runtime will only process the next entry in the event loop when the current entry (i.e. the code containing the loop) terminated. This is called "run to completion". See https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop . `setTimeout` only guarantees that the function is not executed sooner than the provided timeout. – Felix Kling Jun 25 '20 at 11:42
  • 1
    The value of 6 has nothing to do with the issue with setTimeout you are talking about. – user120242 Jun 25 '20 at 11:43
  • @user120242 I see, but okey. To me it seems that using a recursive loop is much better than using a for-loop when messing with setTimeout. – lukact Jun 25 '20 at 11:59
  • 1
    JavaScript is single threaded. asynchronous is not the same as multithreaded parallelism. asynchronous execution is deferred, not run concurrently with the current operations. – user120242 Jun 25 '20 at 12:02
  • @user120242 Yea, I completely forgot that. I was thinking all the time that the for loop would stop and wait for the setTimeout to timeout and then continue for some reason. Since that's how it works when it's a "normal" function there. – lukact Jun 25 '20 at 12:06

3 Answers3

5

Because var is hoisted. When you define a var in the for loop, the variable is defined at the top of the for loop.

var i = undefined;
for(i = 1; i <= 5; i++) { 
  setTimeout(function(){ 
    console.log(i) 
    }, 1000); 
}

The callback function of the setTimeout forms a closure with variable i. The closures are resolved when the callback is executed. Since each for iteration registered a callback that will be evaluated after the for loop exits, the value of i is 6 at that point.

You should use let instead of var which is block-scoped and doesn't hoist. Every iteration of for receives has its own i which will form the closure with the callback. With this, you will receive the desired result.

for(let i = 1; i <= 5; i++) { 
  setTimeout(function(){ 
    console.log(i) 
    }, 1000); 
}


1
2
3
4
5

Event loop in a nutshell

The event loop is a piece of software in the browser (or in Node) that runs continuously in the background. Its job is to provide callback functions to the JavaScript engine (such as V8 in Chrome) to execute. The setTimeout function is a Web-API provided by the browser that sends the callback function to the (event-queue) task-queue to be executed inside the JavaScript engine.

The event loop picks a callback from the task-queue and pushes it on-to the call stack of the JavaScript engine, but it can only do that when the call-stack is empty. Each for loop iteration pushes setTimeout call to the call-stack where it gets executed. Hence unless all for-loop iterations are done, the call-stack is never empty. Therefore, all callbacks enqueued in the task-queue get executed in the end. It really doesn't matter what time you choose for the setTimeout call.

I have written this Medium article where I've discussed the event loop in detail.

Links
Uday Hiwarale
  • 4,028
  • 6
  • 45
  • 48
  • Thanks for replying, but I know that the cause of this is using var and that I can solve it with let. I just don't understand the theory behind this. How come that setTimeout runs solely after the for loop is done. – lukact Jun 25 '20 at 11:48
  • 1
    OP is confused why `setTimeout(fn, 1)` executes before the loop finishes, not that much about why `i` is itself incorrect. – VLAZ Jun 25 '20 at 11:48
  • 1
    Best answer ! I wouldn't have done better than this ;) – r0ulito Jun 25 '20 at 12:09
-1

That happens because of the Call Stacks, Event Loops and Task Queues.

Here is an amazing video to help you understand it visually.

What is happening under the hood?

Firstly, When the javascript interpreter reaches the for loop, the for loop gets added to the Call stack. During the loop execution, when the setTimeout() function is hit, the browser runs the function and add the callback function to what is called the Task Queue.

You may ask, why is the callback function added to task queue and not the call stack? That's because the interpreter is still busy executing the loop. Since javascript is a single-threaded language, it can only execute one piece of code at a time.

Eventually, until the loop ends, the callbacks from setTimeout() are added to the Task Queue. Once the loop ends, the call stack will be empty and is ready to execute the next task. This is where the Event Loop comes into the picture.

Whenever the Event Loop sees that the call stack is empty, it's only job is to pick up the next task from the task queue and add it to the call stack.

All the callbacks in the Task Queue are executed in the order they have added to the queue.

That's the reason, why you see below printed after the loop ends.

6
6
6
6
6

I suggest everyone watch the video link to get a better visual understanding of what I have written.

Harish
  • 714
  • 5
  • 11
-1

One alternative to avoid using let and sticking to var if you need compatibility with stuff that doesn't use ES6.

Enclosing the scope with function separates the variables and avoids automatic hoisting with already existing once since we no longer have access to the i variable from inside the function.

for (var i = 1; i <= 5; i++) {
    // Scope enclosure
    var f = function () {
        return function (x) {
            setTimeout(function () {
                console.log(x)
            }, 1000);
        }
    };
    f()(i);
}
jakob_a
  • 91
  • 1
  • 6