-1

I have been learning ES6 for a week, and I don't understand the behaviour of this code.

    var elements = ["alfa", "beta", "gamma"];
for ( letter of elements){
  setTimeout(function printer(){console.log(letter); }, 0);
  }

I know about differences between let, const and var, but this piece doesn't use any of those and I know when using let declaration within the loop I get all three outputted in the console. But when using var, both outside the loop and inside and when not using anything as above in the code I get three times gamma. I don't understand where gamma is coming from, since letter isn't declared. What are the steps of execution in the code and where is it fetching the last element of the array.

EDIT: The question is NOT how to iterate all elements, but why the code behaves the way it does. I know I can use let to iterate all elements. I wanna know the steps of execution of this 'poorly' written code. Why am I getting 3 times gamma?

Thanks

Steven Spungin
  • 27,002
  • 5
  • 88
  • 78
Pitagora
  • 47
  • 1
  • 9
  • You know how to declare a variable, you used `var elements`. So why aren't you declaring `letter` properly as well using `var`? Be consistent. Anyway you should either use `let` or `const` because they use lexical scoping, not function scoping. – mpm Sep 09 '18 at 19:59
  • if you don't "declare" `letter`, it defaults to being a global variable. Undeclared globals are almost always a really bad idea (and quite frequently done accidentally. Use ["use strict"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode) to make sure you don't do it accidentally) – David784 Sep 09 '18 at 20:00
  • My questions isn't how to correctly iterate all elements, but what is the behaviour of the code and why. I know that by using let I can iterate all elements in an array, I wanna know why this code behaves the way it does an how. – Pitagora Sep 09 '18 at 20:02

2 Answers2

4

Use let (not var) in your loop. It's a closure thing... This makes each iteration use a different value. Using var reuses the value. And because it's a timeout, the last iteration overwrites before the timeout is invoked.

So letter is set to 'alfa', then 'beta', then 'gamma', then the timeout is invoked 3x from the same variable that is now 'gamma'.

Using let forces runtime to reserve a separate variable for each iteration of the loop. Using var will reuse, and not specifying will reuse a global variable.

var elements = ["alfa", "beta", "gamma"];
for (let letter of elements) {
  setTimeout(function printer() {
    console.log(letter);
  }, 0);
}
Steven Spungin
  • 27,002
  • 5
  • 88
  • 78
  • I already mentioned in the OP that using let iterates all. My question is about the behaviour of the code in the op. Why is it outputting only the last element? Is this a pattern? What are the steps of execution, for each iteration. Since letter is undeclared, where is the last element fetched from, in order to be printed 3 times in the console. – Pitagora Sep 09 '18 at 20:08
  • 1
    I answered that the last loop iteration overwrites the variable before the timeout is invoked. – Steven Spungin Sep 09 '18 at 20:08
  • Thanks, that answers my question :) – Pitagora Sep 09 '18 at 20:14
  • setTimeout does not pause code execution until it resolves, what’s more there is a minimum delay of something like 25ms for a setTimeout (ie, you put zero, but you get 25). So the for loop completes (and the value of “letter” is gamma) long before any setTimeout triggers. – James Sep 09 '18 at 20:44
  • @James doesn't answer clearly state this in paragraph 2? – Steven Spungin Sep 09 '18 at 20:47
  • @StevenSpungin Your answer is great. I was trying to provide more details to the follow-up questions in the comments. – James Sep 09 '18 at 20:52
  • @James Thanks for clarifying! I was not sure if comment was meant for me or OP – Steven Spungin Sep 09 '18 at 20:54
0

Well you did ask what was going on - so let me try to explain in detail :)

So in your original code, you have (implicitly) declared letter with the var keyword, and then set up 3 functions to run after the appropriate timeout intervals are complete. When the browser comes to execute these functions, and looks up the value of the letter variable that it's asked to print out, it sees that it has the value "gamma" - because by now the loop is long since complete, and so the loop variable letter still has its final value form the last iteration of the loop .

That has been explained by others above, but you still might (and should!) be wondering - "why does it work differently with let?".

The answer is that I glossed over something slightly when I innocently said that the browser "looks up the value of the letter variable". Because it can only do so within a scope. When you use var to declare a variable in JS, it is in the scope of the entire function it is inside (or the global scope if it isn't in one) - and obviously there is only one of it, so its value gets overwritten as the loop proceeds.

But when you use let, as you may well know, that is scoped to the { } block it is inside - which could be a function, but also a loop, the body of an if statement, or even anywhere else you've chosen to put a block of code within { } (this is legal JS grammar). This is useful quite often, but doesn't appear to impact your code.

What you may not know is that let behaves in a special way when it's used to initialise the variable in a for loop - and this is that it is scoped to the loop block itself, and moreover (and this is the key here), it is effectively redeclared for each loop iteration. That is, it's as if you'd written you're loop out "longhand", with a {..} block around each of the 3 iterations, and with a let letter = ... declaration at the top of each block.

This is why the loop outputs what you wanted it to when you use let in the header - because when, long after the loop is over, the browser looks up the value of the letter variable it's supposed to be printing out, the particular "instance" of the variable it's looking up is bound only to the particular loop iteration it was declared in. So this is why the value isn't over-ridden, as it was when you used var - the callback functions passed to setTimeout "close" over a different "instance" of the letter variable, rather than just one for the whole loop.

In case this still seems like "magic" - it really isn't, although it is a syntactic convenience for the let keyword, which I believe was introduced specifically because it was so common for developers to scratch their heads over loops like yours not doing what people expected. But it's relatively easy to fix (when you know what's going on!) even without let available, like this:

for (var letter of elements) {
    var thisLetter = letter;
    (function(letter) {
        setTimeout(function printer(){console.log(letter); }, 0);
    })(thisLetter);
}

That (function(..) {..})(..) is called an "Immediately Invoked Function Expression", or IIFE for short - it simply defines a function then immediately executes it. Because functions create a new scope, doing it this way means that the letter variable is in a new scope for each function passed to setTimeout - which is exactly what happens when you use let. And, even though let has made this particular construction obsolete (unless you still have to support non-ES6 browsers*), I would say that it's very useful to understand about IIFEs and closures, because they come up all the time in Javascript. And there are loads of well-written articles on the web about them (which is the only reason I know this stuff, I only started learning to code less than 2 years ago).

*before someone tries to be pedantic (and how many developers aren't :p), yes I know this example features for..of and therefore only runs in ES6 environments anyway. But the exact same issue can - and does - come up with for..in and regular for loops.

Robin Zigmond
  • 17,805
  • 2
  • 23
  • 34