JAVASCRIPT / SCOPE, CLOSURES, AND THIS
Loop variables and the classic closure bug
You can explain why every function created in a var loop sees the same final value, and fix it with let, an IIFE parameter, or a per-call callback.
What you will learn
- Predict what functions built inside a var loop return once the loop has finished
- Use let in the for head to get one fresh binding per iteration
- Repair legacy var loops with an IIFE parameter or a let copy in the body
- Tell binding capture apart from value capture when debugging callbacks
Understanding Loop variables and the classic closure bug
A closure keeps a reference to the variable it came from, not a copy of the value that variable held when the function was created. `var i` in a for loop declares one variable for the whole surrounding function, so every function created inside the loop points at the same box while the loop keeps overwriting it. When those functions finally run, the loop has already finished and the box holds the value that failed the test, which is why a loop over 0, 1, 2 produces three functions that all report 3.
Declaring the counter with `let` in the loop head changes the arithmetic of bindings, not the timing: the engine makes a fresh binding for each iteration and copies the current value into it before the update expression runs. Three iterations therefore produce three separate variables, and each closure captures a different one, giving 0, 1, 2. The same per-iteration scoping is why `let` leaves nothing behind after the loop, so reading the counter afterwards throws a ReferenceError, while `var` leaves it visible at its final value.
Before `let` existed the fix was to build a scope by hand, since every function call gets its own parameters and locals: passing `i` into an immediately invoked function, or letting `forEach` hand the index to a callback, produces one fresh variable per iteration. The diagnostic question is never "when does this callback run" but "how many times is this variable created", and the fully synchronous version of the bug proves timing is not involved. A per-iteration binding also fixes only the variable, not what it points at: reassigning that binding later in the same iteration, or mutating an object stored in it, is still visible to the closure.
const varFns = [];
for (var i = 0; i < 3; i++) {
varFns.push(() => i);
}
const letFns = [];
for (let j = 0; j < 3; j++) {
letFns.push(() => j);
}
console.log('var captures:', varFns[0](), varFns[1](), varFns[2]());
console.log('let captures:', letFns[0](), letFns[1](), letFns[2]());
console.log('i is still visible after the loop:', i);A closure captures a variable binding rather than a snapshot of its value, so the cure for the loop bug is making the loop create a new binding each time round.
Worked examples
Hand-built scope with an IIFE
Shows the pre-let fix: pass the counter into a function call so each iteration gets its own parameter.
function makeHandlers() {
const handlers = [];
for (var i = 0; i < 3; i++) {
(function (captured) {
handlers.push(function () {
console.log('handler for index ' + captured);
});
})(i);
}
return handlers;
}
const hs = makeHandlers();
hs[0]();
hs[2]();Example explained
Line 1The wrapper function is called immediately, so it receives the value `i` has during that iteration.
Line 2`captured` is a parameter, and parameters are created afresh on every call, so three calls mean three separate variables.
Line 3The pushed function closes over `captured`, which nothing ever reassigns, instead of over the shared `i`.
Line 4`i` itself still ends at 3, but no stored function looks at it any more.
The setTimeout symptom
Demonstrates the value the shared var binding holds by the time asynchronous callbacks run.
for (var i = 1; i <= 3; i++) {
setTimeout(function () {
console.log('var i =', i);
}, 0);
}
console.log('sync line runs first, i =', i);Example explained
Line 1All three timers are only queued during the loop; the synchronous log below runs before any of them fire.
Line 2`i` is 4 because the loop exits after the increment that makes the test `i <= 3` false.
Line 3Every callback reads the same binding at fire time, so all three print 4 rather than 1, 2, 3.
Line 4Changing `var i` to `let i` prints 1, 2, 3 without touching the delay.
Callback parameters and for...of
Shows two loop forms that give per-iteration variables without any manual copying.
const words = ['ant', 'bee', 'cow'];
const getters = [];
words.forEach(function (word, index) {
getters.push(function () { return index + ':' + word; });
});
for (const w of words) {
getters.push(function () { return 'of:' + w; });
}
console.log(getters[0](), getters[1](), getters[2]());
console.log(getters[3](), getters[4](), getters[5]());Example explained
Line 1`forEach` invokes its callback once per element, and each invocation creates new `word` and `index` bindings.
Line 2The inner function closes over those per-call parameters, so no two stored getters share a variable.
Line 3`for (const w of words)` declares `w` once per iteration, which is why `const` is legal here even though the value differs each time.
Line 4Neither form needs an IIFE, because both already create a new binding per pass.
Important notes
`for (const i = 0; i < 3; i++)` throws "TypeError: Assignment to constant variable" on the first increment; `const` is only usable as a loop variable in `for...of` and `for...in`, where nothing reassigns it.
A per-iteration binding freezes the variable, not the object it holds — if each iteration stores the same object and you mutate it afterwards, every closure sees the mutation.
Common mistakes
Blaming the delay and spreading the timers out with `i * 100`: the output is still all 3s, and the timing change disguises a scoping bug as a race.
Writing `let i; for (i = 0; i < 3; i++)` instead of `for (let i = 0; ...)`: the declaration is outside the loop head, so there is one binding again and every closure still sees the final value.
Copying the counter with `var copy = i;` inside the body: `var` hoists to the function, so all iterations write to one `copy` and every callback returns the last value.
Try it yourself
Change, predict, then run
In the browser console, fill an array with three functions using a `var` loop and log their results, then change only the declaration to `let` and log again. Finally reproduce the `let` behaviour without `let` by wrapping the loop body in an immediately invoked function that takes the index as a parameter.
Open the JavaScript workspaceCheck your understanding
A loop pushes arrow functions into an array. With `var i` every function returns 3; with `let i` they return 0, 1 and 2. What actually changed?
- `let` creates a separate binding on each iteration, so each function closes over a different variable
- `let` copies the current value into the function when the function is created, while `var` reads the variable later
- `let` makes the stored functions run before the loop's final increment happens
- `let` makes the counter read-only inside the loop body, so the increment cannot affect the captured value
Show answer
Closures always capture bindings, never values; the only difference is how many bindings the loop creates — one for the whole function with `var`, one per iteration with `let`. Option 2 is tempting because the results look like snapshots, but nothing is copied into the function: if you reassign the per-iteration counter later in the same iteration, the stored function reports the new value.