skip to content

Before `let` existed, how did you make `for (var i = 0; i < 3; i++) setTimeout(function () { console.log(i); }, 0);` log 0, 1, 2 without changing `var`? Show the wrapper and say what actually makes it work.

level: middleimportance: should knowfreq 42%

answer

  1. needs a per-iteration snapshot
  2. function call creates the new binding
  3. the parameter is doing the work
  4. bind freezes arguments at bind time
  5. let makes the idiom obsolete

basics

~20 s

Wrap the body in an immediately-invoked function that takes the counter as a parameter. Argument passing copies the value into a fresh parameter binding per call, so the scheduled callback closes over that copy rather than the one shared var. Function.prototype.bind achieves the same by fixing the argument.

solid answer

~40 s

The pre-ES6 idiom is an IIFE with a parameter: `for (var i = 0; i < 3; i++) { (function (n) { setTimeout(function () { console.log(n); }, 0); })(i); }`. What does the work is not the wrapping but the **argument passing** — each call creates a new function environment with its own `n`, initialised to a copy of `i` at that moment, and the timer callback closes over `n`. An IIFE that takes no parameter and still reads `i` fixes nothing. Two other value-copying forms work the same way: `setTimeout(console.log.bind(null, i), 0)` fixes the argument at bind time, and `setTimeout(console.log, 0, i)` uses the extra arguments the timer forwards to the callback. In modern code you would just use `let`, or copy into a `const` inside the loop body.

code

javascript · 11 lines
javascript
for (var i = 0; i < 3; i++) {
  (function (n) {
    setTimeout(function () { console.log('iife', n); }, 0);
  })(i);
}

for (var k = 0; k < 3; k++) {
  setTimeout(console.log.bind(null, 'bind', k), 0);
}

// iife 0, iife 1, iife 2, bind 0, bind 1, bind 2

go deeper

for a junior

Recognise the wrapper when you see it in older code and be able to say it exists to give each iteration its own copy of the counter, and that let replaced it.

for a middle

Write the IIFE correctly from memory with the parameter, explain that argument passing is what creates the fresh binding, and name bind and timer argument forwarding as equivalents.

for a senior

Point out that the snapshot is of a value, not of data behind a reference, and show the mutation case where the wrapper looks right but the bug persists. Advise on the clearest fix for the codebase in front of you.

for a principal

Decide policy: whether legacy loop wrappers get rewritten in a sweep or left alone, and how you communicate that the underlying rule — per-iteration binding or explicit argument — outlives whichever syntax is fashionable.

## Why a wrapper is needed at all With `var` there is exactly one binding for the counter, shared by every function created in the loop, and every deferred callback reads it after the loop has finished. To fix it without changing the declaration you must give each iteration a **different binding that holds a copy of the value at that moment**. Everything below is a way to manufacture such a binding. ## The IIFE with a parameter ```js for (var i = 0; i < 3; i++) { (function (n) { setTimeout(function () { console.log(n); }, 0); })(i); } // 0, 1, 2 ``` Calling a function creates a new environment for that call, containing its parameters. `n` is a fresh binding per invocation, initialised by copying the argument's value — and for a number, copying the value is all there is. The timer callback closes over `n`, which nothing ever reassigns, so it reports that iteration's number. The most common mistake is dropping the parameter: ```js for (var i = 0; i < 3; i++) { (function () { setTimeout(function () { console.log(i); }, 0); // still 3, 3, 3 })(); } ``` The wrapper runs, a new environment is created — but nothing in it is named `i`, so the lookup walks out to the same shared `var` binding. Calling a function is not what fixes the bug; **passing the value as an argument** is. The same trap appears when the parameter exists but the inner function reads `i` instead of `n`. ## bind, which freezes arguments `Function.prototype.bind` returns a new function with `this` and any leading arguments fixed at bind time: ```js for (var i = 0; i < 3; i++) { setTimeout(console.log.bind(null, i), 0); } // 0, 1, 2 ``` The value of `i` is read when `bind` is called — during that iteration — and stored inside the bound function. Later mutation of the outer binding is irrelevant because nothing looks it up again. This form is compact, but it forces the callback to be a function you can bind arguments onto, which does not always fit. ## Argument forwarding Timers forward any arguments after the delay to the callback: ```js for (var i = 0; i < 3; i++) { setTimeout(function (n) { console.log(n); }, 0, i); } // 0, 1, 2 ``` Same mechanism as the IIFE — a parameter binding created per call, initialised from a value read at scheduling time — with the function call supplied by the timer instead of written by hand. ## The modern equivalents Both of these are what you would actually write today: ```js for (let i = 0; i < 3; i++) setTimeout(() => console.log(i), 0); // per-iteration head binding for (var i = 0; i < 3; i++) { const n = i; // fresh block binding per pass setTimeout(() => console.log(n), 0); } ``` The `const` copy is worth knowing even in modern code: it is the fix when the counter must stay `var` (say, because something after the loop reads it), and it is the same idea as the IIFE parameter with none of the extra function. ## The important caveat: copying a reference is not copying data All of these copy a **value**. If the value is an object reference, every iteration's binding still points at the same object, and mutating that object later is visible through all of them: ```js var cfg = { id: 0 }; for (var i = 0; i < 3; i++) { (function (c) { setTimeout(function () { console.log(c.id); }, 0); })(cfg); cfg.id = i; } // 2, 2, 2 — one object, three references to it ``` The wrapper did its job; the problem moved. If each iteration needs its own data, create the object inside the iteration, or copy the fields you care about. ## What to say in an interview Name the mechanism, not the ritual: each iteration needs its own binding holding a snapshot, and a function call is simply the pre-ES6 way to manufacture one. Then note that `let` builds the same thing into the loop head, which is why the idiom is now history rather than practice.

  • Why does an IIFE that takes no parameter fail to fix the loop?
    Because the fix comes from argument passing, not from the extra call. A parameterless wrapper creates an environment containing nothing named `i`, so the inner function's lookup walks out to the same shared `var` binding and still reads the final value.
  • Does the IIFE approach protect you when the captured value is an object?
    Only against the loop variable being reassigned. The parameter holds a copy of the reference, so all iterations still point at one object; mutating it afterwards is visible to every callback. Per-iteration data means constructing a new object inside the iteration.
  • Is there any reason to still reach for these workarounds in modern code?
    Rarely as workarounds — `let` or a `const` copy is clearer. `bind` and argument forwarding survive on their own merits: fixing an argument onto an existing function, or avoiding an extra closure allocation in a hot path where you would otherwise create a wrapper per call.

saying these in an interview costs you the question

  • Says any IIFE fixes it, parameter or not
  • Thinks the wrapper delays the callback
  • Claims bind copies the whole surrounding scope
  • Believes the copy protects against object mutation
  • Calls the IIFE workaround current best practice

context