setTimeout
in a loop
While we're still at passing values to timer functions, let's take a little detour for a once common interview question: calling setTimeout
in a loop. (I only hope it's not common any more. I never asked that anyone in the interview.)
A candidate would be presented with the first loop and a question what would happen. The correct answer is that it will print five times 5 in one-second breaks (more or less, as setTimeout cannot guarantee the exact time). People who don't fully understand hoisting, which happens when you use var
, will correctly point to one-second breaks, but will miss the logging part. Ah, gotcha, junior!
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i); // <-- by the execution time, this will be 5
}, i * 1000);
}
The second part of the task is to fix it.
Back in the days, the usual solution was a closure that copies the value and uses it without interference from the outside.
for (var i = 0; i < 5; i++) {
function closure(value) {
setTimeout(function () {
console.log(value);
}, value * 1000)
}
closure(i);
}
Function method bind
can be used as well.
for (var i = 0; i < 5; i++) {
setTimeout(console.log.bind(null, i), i * 1000);
}
But, knowing about additional argument, we can just pass it this way (it's a primitive value, so it ends up being copied, not passed as a reference). The resulting code is actually the tersest of 'em all.
for (var i = 0; i < 5; i++) {
setTimeout(console.log, i * 1000, i);
}
The life, however, wrote a new chapter to this hectic story. While var
is hoisted, let
and const
are not. They are block-scoped and this somehow results in each iteration having access to the value as per its iteration. So, by swapping var
to let
, we make the original snippet working.
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1_000);
}
And that's it for today. Thank you for coming to my tech talk.