Exercise
This is a classic scope puzzle. Using var, all callbacks share the same variable, so they all log the final value. The IIFE fix creates a new scope for each iteration.
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log('var:', i);
}, 100);
}
// Output: var: 3, var: 3, var: 3
// Fix using IIFE to capture each value
for (var j = 0; j < 3; j++) {
(function(captured) {
setTimeout(function() {
console.log('IIFE:', captured);
}, 100);
})(j);
}
// Output: IIFE: 0, IIFE: 1, IIFE: 2
In non-strict mode, eval() can introduce new variables into the calling scope. In strict mode, eval() has its own scope and cannot modify the surrounding scope. Using eval() is generally discouraged because it makes scope unpredictable.
function nonStrictExample() {
eval('var x = 42');
console.log(x); // Output: 42 (eval injected into function scope)
}
nonStrictExample();
function strictExample() {
'use strict';
eval('var y = 42');
console.log(y); // ReferenceError: y is not defined
}
strictExample();
Each recursive call creates a new execution context with its own scope. When closures are involved, each call's scope is preserved independently. This means inner functions in recursive calls capture different variable values.
function buildMultipliers(n) {
if (n === 0) return [];
var result = buildMultipliers(n - 1);
result.push(function() { return n * 2; });
return result;
}
var fns = buildMultipliers(3);
console.log(fns[0]()); // Output: 2 (n was 1)
console.log(fns[1]()); // Output: 4 (n was 2)
console.log(fns[2]()); // Output: 6 (n was 3)
Async functions create their own scope like regular functions. When closures capture variables used in async operations, the variable values at the time of the await resolution matter. Variables from the enclosing scope remain accessible even after asynchronous pauses.
async function fetchAll() {
var results = [];
for (let i = 0; i < 3; i++) {
results.push(
new Promise(resolve => {
setTimeout(() => resolve(i), 10);
})
);
}
var values = await Promise.all(results);
console.log(values); // Output: [0, 1, 2]
}
fetchAll();
When multiple closures reference the same variable from an outer scope, they all share a single binding. Changes made through one closure are visible to all others.
function createActions() {
var value = 0;
return {
increment: function() { value++; },
decrement: function() { value--; },
getValue: function() { return value; }
};
}
var actions = createActions();
actions.increment();
actions.increment();
actions.increment();
actions.decrement();
console.log(actions.getValue()); // Output: 2
The with statement extends the scope chain by adding an object's properties as local variables. This makes scope unpredictable because property lookups and variable references become ambiguous. The with statement is deprecated and forbidden in strict mode.
var obj = { x: 10, y: 20 };
var x = 'global';
with (obj) {
console.log(x); // Output: 10 (resolved from obj, not global)
console.log(y); // Output: 20
z = 30; // creates global variable (not on obj)
}
console.log(x); // Output: global
console.log(z); // Output: 30
Each nested function creates a new link in the scope chain. The innermost function can access variables from all enclosing functions. The scope chain is determined at definition time, not at call time.
function level1() {
var a = 1;
return function level2() {
var b = 2;
return function level3() {
var c = 3;
return function level4() {
console.log(a + b + c); // Output: 6
a = 10;
return a + b + c;
};
};
};
}
var result = level1()()()();
console.log(result); // Output: 15
Generator functions create a new scope for each invocation, just like regular functions. However, because generators can pause and resume execution, their local variables persist between yield calls. Closures inside generators work the same way.
function* scopeGenerator() {
var count = 0;
while (true) {
count++;
var multiplier = yield count;
if (multiplier) {
count = count * multiplier;
}
}
}
var gen = scopeGenerator();
console.log(gen.next().value); // Output: 1
console.log(gen.next().value); // Output: 2
console.log(gen.next(10).value); // Output: 21
This puzzle combines hoisting of var declarations, closure over function-scoped variables, and the difference between declaration and assignment timing. Understanding execution order is key.
var funcs = [];
function setup() {
for (var i = 0; i < 3; i++) {
var x = i * 10;
funcs.push(function() {
return x + i;
});
}
}
setup();
console.log(funcs[0]()); // Output: 23 (x is 20, i is 3)
console.log(funcs[1]()); // Output: 23
console.log(funcs[2]()); // Output: 23
Functions created with the Function constructor do not capture the local scope of the context where they are created. They always execute in the global scope. This is different from function declarations and expressions, which capture their lexical scope.
var globalValue = 'global';
function createFunctions() {
var localValue = 'local';
var normalFunc = function() {
return typeof localValue; // captures local scope
};
var constructedFunc = new Function(
'return typeof localValue' // runs in global scope
);
console.log(normalFunc()); // Output: string
console.log(constructedFunc()); // Output: undefined
}
createFunctions();