JAVASCRIPT / VARIABLES: VAR, LET, AND CONST
var quirks and function scope
You can pinpoint which function a var belongs to, predict its value outside the block it was written in, and fix loops whose closures share one counter.
What you will learn
- Name the exact scope of any var: the whole nearest function body, never the block.
- Explain why a var read after its if block gives undefined instead of a ReferenceError.
- Spot loop closures that all read one shared var counter and predict their value.
- Recreate per-iteration bindings with an IIFE when var cannot be replaced.
Understanding var quirks and function scope
A var declaration attaches its name to the nearest enclosing function body, or to the global scope when no function encloses it. Nothing smaller counts: if, for, while, switch, try and standalone { } blocks are all transparent to var. So a var written deep inside three nested blocks is, as far as the engine is concerned, a variable of the entire function, alive from the moment the call begins until it returns.
That single rule explains the behaviour people find surprising. Read the name after the block that seemed to own it and you get a value, or undefined if the assignment never ran, but never an error, so a branch that was not taken leaves a quiet undefined that breaks something much later. Two var declarations of the same name in different branches of one function are also one storage slot, so whichever assignment runs last is the value everyone sees.
The counting rule is per function call, not per block and not per iteration. A for (var i = ...) loop creates exactly one i for the whole loop, so every function created in the body closes over that one variable and reads whatever it holds when it is eventually called, which is normally the value that ended the loop. The only way to get a second copy with var is to enter a function again, which is why pre-2015 code wraps loop bodies in an immediately invoked function, while let builds a fresh per-iteration binding into the loop itself.
test
function checkStock(count) {
if (count > 0) {
var status = 'in stock';
}
// status is visible here: it belongs to checkStock, not to the if block
return status;
}
console.log(checkStock(5));
console.log(checkStock(0));
var readers = [];
for (var i = 0; i < 3; i++) {
readers.push(function () {
return i; // reads the one shared i, not a snapshot
});
}
console.log(readers.map(function (r) { return r(); }).join(','));
console.log('i after the loop:', i);A var declaration belongs to the entire nearest enclosing function, so one call to that function means one binding no matter how many blocks or iterations touch it.
Worked examples
Nested loops fighting over one counter
Two loops in the same function both declaring var i share a single counter, while let gives the inner loop its own.
function grid(rows, cols) {
var cells = [];
for (var i = 0; i < rows; i++) {
for (var i = 0; i < cols; i++) {
cells.push(i);
}
}
return cells.join(',');
}
function gridLet(rows, cols) {
const cells = [];
for (let i = 0; i < rows; i++) {
for (let i = 0; i < cols; i++) {
cells.push(i);
}
}
return cells.join(',');
}
console.log(grid(2, 3));
console.log(gridLet(2, 3));Example explained
Line 1The inner for (var i = 0; ...) does not create a second variable; it reassigns the same i the outer loop is counting with, because both belong to grid's function scope.
Line 2When the inner loop stops, i is 3, so the outer i++ makes it 4 and the outer test 4 < 2 fails after one pass, producing three cells instead of six.
Line 3In gridLet the inner let i is a separate binding that shadows the outer one, so the outer counter is never disturbed and all six cells appear.
Line 4Call grid(5, 2) instead and the var version never terminates, because the inner loop keeps resetting the outer counter below its limit.
Blocks share, functions do not
Shows that a var inside a block overwrites the outer one while a var inside a nested function is a genuinely different variable.
function outer() {
var scope = 'outer';
if (true) {
var scope = 'block';
}
function inner() {
var scope = 'inner';
return scope;
}
return inner() + ' / ' + scope;
}
console.log(outer());Example explained
Line 1var scope = 'block' writes into the same slot as the first declaration, since outer has only one scope for var and the if contributes none.
Line 2var scope = 'inner' is a different variable because a function body does start a new scope, so inner cannot disturb outer's value.
Line 3The result shows both facts at once: the block's write survived as 'block', and inner kept its private 'inner'.
One binding per call, via an IIFE
Recovers per-iteration values with var by entering a function on every pass of the loop.
var fns = [];
for (var i = 0; i < 3; i++) {
(function (n) {
fns.push(function () { return n; });
})(i);
}
console.log(fns.map(function (f) { return f(); }).join(','));Example explained
Line 1(function (n) { ... })(i) is called immediately, and each call builds a fresh scope whose parameter n holds a copy of i as it was at that moment.
Line 2The pushed function closes over n rather than the single shared i, so each stored function keeps its own number.
Line 3for (let i = 0; ...) gives the same 0,1,2 with no wrapper, because the loop itself creates a new i binding per iteration.
Important notes
At the top level of a classic browser script the global scope is var's home, so var total = 1 also creates globalThis.total, which let and const never do; inside a Node CommonJS module or an ES module a top-level var stays local to that module.
A parameter and a var of the same name are one binding: in function f(x) { var x; return x; }, f(5) returns 5, because a var declaration without an initializer never overwrites an existing value.
Common mistakes
Assuming a var stops existing at the closing brace of its if or for block; later code reads the name, receives undefined instead of a ReferenceError, and the failure shows up far away from the line that caused it.
Reusing var i for an inner loop in the same function, which resets the outer counter so the outer loop runs once, skips rows, or spins forever.
Registering setTimeout callbacks or event handlers inside for (var i = 0; ...) and expecting each to remember its own index; they all read the counter's final value because there is only one i.
Try it yourself
Change, predict, then run
In a browser console, fill an array with three functions that return i using for (var i = 0; i < 3; i++), call all three, then log i after the loop. Change only var to let and record how each of those two logs changes.
Open the JavaScript workspaceCheck your understanding
Wrapping the body of a for (var i = 0; i < 3; i++) loop in an immediately invoked function that takes i as a parameter makes each stored callback report its own number. Why does that work?
- The wrapper runs synchronously during the iteration, which freezes the value of i at that moment for everything defined inside it.
- Calling the wrapper creates a new scope on every iteration, so its parameter is a separate variable that each callback closes over instead of the shared i.
- Code inside a function expression is block scoped, so i behaves there as if it had been declared with let.
- Passing i as an argument turns it into a constant, and constants cannot be changed by the loop's i++.
Show answer
Each call to a function builds a new set of bindings, so every iteration gets its own parameter holding a copy of the counter, and the callbacks close over those distinct variables while the outer i stays a single var for the whole loop. The first option is tempting, but timing is not the mechanism: a closure reads its variable when it runs rather than when it is created, so if the wrapper's parameter were the same binding as i, calling the wrapper early would change nothing.