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.