JavaScript let and const Per-Iteration Bindings
In JavaScript, `let` and `const` declared in certain loop headers can produce a distinct binding for each iteration. This behavior is especially important when closures capture the loop variable, because each callback can retain the value from its own iteration.
A lexical declaration such as `let` or `const` is block-scoped. In `for` loops where the loop variable is declared with `let` or `const`, JavaScript can create a fresh binding for each iteration when the loop body is evaluated. A closure created during that iteration captures that iteration's binding.
This explains why callbacks created in a `for` loop with `let` usually observe different values. For example, `for (let i = 0; i < 3; i++)` gives callbacks created during the three iterations access to bindings corresponding to `0`, `1`, and `2` rather than one shared mutable binding.
The distinction becomes important with asynchronous callbacks. If `setTimeout(() => console.log(i), 0)` is created inside a loop using `let`, each callback retains the binding associated with its iteration. With `var`, the loop normally uses a function-scoped binding instead, so callbacks commonly observe the final value after the loop finishes.
`const` follows the same per-iteration idea when declared in an appropriate loop construct, although the binding itself cannot be reassigned within an iteration. For example, `for (const item of items)` gives each iteration its own lexical `item` binding, which can safely be captured by a closure.
Key points
- `let` and `const` loop bindings can be recreated for each iteration.
- Closures capture the lexical binding available in their iteration.
- `var` does not provide the same per-iteration lexical binding behavior.
- This is especially visible with callbacks and asynchronous code.
- `const` prevents reassignment of its binding within an iteration.
Interview questions
Why does `let` work differently from `var` when used in a loop with callbacks?
`let` is block-scoped and, in applicable loop forms, JavaScript creates a distinct lexical binding for each iteration. A callback created during an iteration closes over that iteration's binding. With `var`, the loop generally uses one function-scoped binding that is repeatedly updated. As a result, callbacks created with `var` commonly see the final loop value.
What does a closure capture in a `for (let ...)` loop?
A closure captures the lexical binding, not merely a copied primitive value. In a loop with `let`, the relevant loop semantics provide a distinct binding for each iteration. Therefore, callbacks created in different iterations can refer to different bindings. This is why they can later observe different values.
Does `const` also get a new binding for each iteration?
Yes, in loop forms that create per-iteration lexical bindings, `const` receives a binding associated with each iteration. The important difference from `let` is that a `const` binding cannot be reassigned within that iteration. This makes constructs such as `for (const item of items)` natural for closure capture when the loop variable should not change within an iteration.
Why does a callback created inside a `let` loop remember the correct iteration after the loop finishes?
The callback closes over the lexical environment associated with the iteration in which it was created. Applicable `for` loop semantics create a new iteration-specific binding, and the callback retains access to that binding after the iteration has ended. Later execution of the callback therefore resolves its variable reference against the corresponding captured environment. This is a consequence of lexical environments and closure semantics, not a special feature of `setTimeout` or promises.