`for (var v = 0; v < 3; v++) setTimeout(() => console.log(v))` logs 3, 3, 3, but `[0, 1, 2].forEach(v => setTimeout(() => console.log(v)))` logs 0, 1, 2 — and neither uses `let`. What accounts for the difference?
answer
- parameters are per-call bindings
- one call, one environment
- the array method supplies the IIFE
- outer variables are still shared
- for...of head is fresh per iteration
basics
~20 sforEach calls its callback once per element, and every call creates a new environment holding its own parameter binding for that element. The counting loop has a single shared var binding instead. Callback-based iteration gets per-iteration bindings for free, whatever declaration keyword you use.
solid answer
~40 sThe difference is where the variable lives. In the `for` loop, `v` is one function-scoped binding created by `var`, shared by every closure the loop creates. In the `forEach` version, `v` is a **parameter** of the callback, and every invocation of a function creates a fresh environment containing its own parameters — three calls, three bindings, each initialised with that element's value. The inner timer callback closes over its own call's binding, so nothing can overwrite it. The same reasoning covers `map`, `filter`, and any other callback-taking method, and it is why the classic loop bug essentially disappeared once codebases moved to array iteration methods. `for...of` with `let` or `const` behaves the same way for a different reason: its head binding is created fresh per iteration.
code
javascript · 8 lineslet shared;
['a', 'b', 'c'].forEach((item, i) => {
shared = item;
setTimeout(() => console.log(i, item, shared));
});
// 0 a c
// 1 b c
// 2 c cgo deeper
Know that a callback parameter is a new variable on every call, which is why values passed into forEach or map are remembered correctly by functions created inside them.
Explain that each function invocation creates its own environment for its parameters, and show that this is the same mechanism as the pre-ES6 IIFE fix with the call supplied by the array method.
Demonstrate the limit of the pattern: variables hoisted outside the callback are still shared, so switching to forEach can hide the bug in one place and leave it in another. Say how you would review for that.
Weigh iteration style as a standard: callback iteration removes a bug class but costs a call per element and cannot break early. Be able to justify where the team should prefer each and why safety alone is not the deciding argument.
## Two loops, two very different scope shapes ```js for (var v = 0; v < 3; v++) setTimeout(() => console.log(v)); // 3, 3, 3 [0, 1, 2].forEach(v => setTimeout(() => console.log(v))); // 0, 1, 2 ``` Neither snippet uses `let`, so the usual one-word explanation does not apply. The real variable is *where the binding comes from*. ## The counting loop: one binding `var v` is hoisted to the enclosing function scope. The loop head does not create a scope, so there is exactly one slot named `v` for the whole program run. Three arrow functions are created, and all three capture the same environment. When they run — after the loop, since timers are deferred — they read whatever that one slot holds, which is 3. ## forEach: one binding per call `Array.prototype.forEach` invokes its callback once per element, passing `(element, index, array)`. Every function **call** creates a new environment containing that call's parameters, initialised from the arguments. So three calls produce three separate `v` bindings, holding 0, 1 and 2 respectively. The timer callback is created inside one of those calls and closes over that call's environment. Nothing ever reassigns those parameters, so each callback reports its own element. Conceptually: ```js const body = (v) => setTimeout(() => console.log(v)); body(0); body(1); body(2); // three calls, three v bindings ``` That is exactly the pre-ES6 IIFE fix, except the array method supplies the call for you. ## The general rule A closure sees the final value only when the binding it captured is **shared and later mutated**. There are three ways to get a private binding: 1. a per-iteration head binding (`for (let i = 0; ...)`, `for (const x of xs)`); 2. a fresh block-scoped declaration inside the body (`const n = i;`); 3. a function call whose parameter carries the value (`forEach`, `map`, an IIFE, `bind`). Counting loops with `var` are the only common shape that provides none of them. ## Things that still bite in callback iteration Using `forEach` does not make everything safe. Variables declared **outside** the callback are still shared: ```js let last; // one binding, outside the callback [0, 1, 2].forEach(v => { last = v; setTimeout(() => console.log(last)); }); // 2, 2, 2 ``` And a `var` declared inside the callback body is scoped to that callback invocation, so it is per-call and safe — `var` is function-scoped, and here the callback *is* the function. The hazard is not the keyword in isolation; it is whether the binding is shared across the deferred functions. Capturing the index behaves identically to the element, since it is also a parameter: ```js ['a', 'b'].forEach((item, i) => setTimeout(() => console.log(i, item))); // 0 a, 1 b ``` ## Choosing between the forms Callback iteration removes a whole bug class, and it reads well for transformation. The counting `for` loop still earns its place when you need to break early, iterate backwards, step by more than one, or mutate the array as you go — `forEach` cannot be broken out of, and `return` inside it only ends that one call. When you do reach for it, declare the counter with `let` and the capture question never arises. ## What an interviewer is checking That you can explain the fix in terms of bindings rather than reciting "use forEach, it's safer". A candidate who says "each invocation gets its own parameter binding, so the closures capture different slots" has the model that also predicts the `last` example above, where switching to `forEach` changes nothing.
- Does declaring a variable with var inside a forEach callback reintroduce the bug?No. `var` is function-scoped, and the callback is the function, so each invocation gets its own binding. The bug needs a binding shared across the deferred functions — one declared outside the callback, in the enclosing scope, would still cause it.
- Does `for (const item of items)` behave like forEach for capture purposes?Yes, though by a different mechanism: a `for...of` head declared with `let` or `const` creates a fresh binding for each iteration, initialised from the iterator. Closures created in the body therefore capture distinct bindings and see their own element, exactly like a callback parameter.
- If forEach avoids the problem, why keep counting for loops at all?Because `forEach` cannot break early or skip ahead — `return` only ends the current invocation — and it does not fit reverse iteration, custom steps, or code that mutates the array as it walks it. With `let` in the head, a counting loop has no capture hazard either, so the choice is about control flow, not safety.
saying these in an interview costs you the question
- Says forEach is synchronous so the bug cannot happen
- Claims array methods declare their variables with let
- Thinks forEach protects variables declared outside the callback
- Believes var inside the callback breaks it
- Says break works inside forEach