Chapter 8: The Scope and Closure Mysteries
· 13 min read
Chapter 8: The Scope and Closure Mysteries
In JavaScript Land, understanding scope and closures is like learning the ancient magic that governs how variables are accessed and remembered.
The Scope Hierarchy
// Global scope - visible everywhere
var globalVar = "I'm global!";
let globalLet = "I'm also global!";
function outerFunction(param) {
// Function scope - visible within this function and nested functions
var functionScoped = "I'm function scoped";
let blockScoped = "I'm block scoped";
console.log(globalVar); // ✅ Can access global
console.log(param); // ✅ Can access parameters
if (true) {
// Block scope - only visible within this block for let/const
var functionScopedVar = "var ignores blocks";
let blockScopedLet = "let respects blocks";
const blockScopedConst = "const also respects blocks";
console.log(functionScoped); // ✅ Can access function scope
console.log(blockScoped); // ✅ Can access outer block scope
}
console.log(functionScopedVar); // ✅ var leaked out of block
// console.log(blockScopedLet); // ❌ ReferenceError - let stayed in block
// console.log(blockScopedConst); // ❌ ReferenceError - const stayed in block
function innerFunction() {
console.log(functionScoped); // ✅ Can access outer function scope
console.log(globalVar); // ✅ Can access global scope
let innerVar = "I'm inner";
// This creates a closure!
}
return innerFunction;
}
// console.log(functionScoped); // ❌ ReferenceError - can't access function scope from global
The Closure Chronicles
A closure is like a magical backpack that a function carries, containing all the variables from its birth environment.
// Classic closure example
function createCounter() {
let count = 0; // This variable is "closed over"
return function() {
count++; // The inner function remembers 'count'
return count;
};
}
let counter1 = createCounter();
let counter2 = createCounter(); // Each gets its own 'count'
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 (independent counter)
console.log(counter1()); // 3
// More complex closure - a function factory
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier; // 'multiplier' is closed over
};
}
let double = createMultiplier(2);
let triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Practical closure example - private variables
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private variable
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
return balance;
}
throw new Error("Deposit amount must be positive");
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return balance;
}
throw new Error("Invalid withdrawal amount");
},
getBalance() {
return balance;
}
// No direct access to 'balance' from outside!
};
}
let account = createBankAccount(100);
console.log(account.getBalance()); // 100
account.deposit(50);
console.log(account.getBalance()); // 150
// console.log(account.balance); // undefined - truly private!
The Dreaded Loop Closure Problem
// Common mistake with closures in loops
console.log("❌ Broken version:");
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Prints 3, 3, 3 - why?
}, 100);
}
// By the time setTimeout executes, the loop has finished and i = 3
// Solution 1: Use let instead of var
console.log("✅ Fixed with let:");
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Prints 0, 1, 2 - each iteration gets its own 'i'
}, 100);
}
// Solution 2: Create a closure with IIFE (Immediately Invoked Function Expression)
console.log("✅ Fixed with IIFE:");
for (var i = 0; i < 3; i++) {
(function(index) {
setTimeout(function() {
console.log(index); // Prints 0, 1, 2
}, 100);
})(i); // Pass current value of i to the IIFE
}
// Solution 3: Use bind
console.log("✅ Fixed with bind:");
for (var i = 0; i < 3; i++) {
setTimeout(function(index) {
console.log(index); // Prints 0, 1, 2
}.bind(null, i), 100);
}
Advanced Closure Patterns
Module Pattern - Encapsulation with Closures
const Calculator = (function() {
// Private variables and functions
let result = 0;
let history = [];
function addToHistory(operation, value) {
history.push({ operation, value, result });
}
// Public API
return {
add(num) {
result += num;
addToHistory('add', num);
return this;
},
subtract(num) {
result -= num;
addToHistory('subtract', num);
return this;
},
multiply(num) {
result *= num;
addToHistory('multiply', num);
return this;
},
getResult() {
return result;
},
getHistory() {
return [...history]; // Return copy to prevent mutation
},
reset() {
result = 0;
history = [];
return this;
}
};
})();
Calculator.add(10).multiply(2).subtract(5);
console.log(Calculator.getResult()); // 15
console.log(Calculator.getHistory()); // Full operation history
Currying with Closures
// Currying - transforming function with multiple arguments into a sequence of functions
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
// Example usage
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6
// Practical currying example
function log(level, timestamp, message) {
console.log(`[${timestamp}] ${level}: ${message}`);
}
const curriedLog = curry(log);
const errorLog = curriedLog("ERROR");
const errorNow = errorLog(new Date().toISOString());
errorNow("Something went wrong!"); // [timestamp] ERROR: Something went wrong!
Memoization with Closures
// Memoization - caching function results
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Returning cached result');
return cache.get(key);
}
console.log('Computing result');
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Expensive operation
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFib = memoize(fibonacci);
console.log(memoizedFib(40)); // Computing result (slow)
console.log(memoizedFib(40)); // Returning cached result (instant)
Scope Chain and Lexical Environment
// Understanding the scope chain
const globalValue = "global";
function outer() {
const outerValue = "outer";
function middle() {
const middleValue = "middle";
function inner() {
const innerValue = "inner";
// Scope chain: inner -> middle -> outer -> global
console.log(innerValue); // ✅ Found in current scope
console.log(middleValue); // ✅ Found in parent scope
console.log(outerValue); // ✅ Found in grandparent scope
console.log(globalValue); // ✅ Found in global scope
}
inner();
}
middle();
}
outer();
// Variable shadowing
let name = "Global";
function greet() {
let name = "Function"; // Shadows global 'name'
if (true) {
let name = "Block"; // Shadows function 'name'
console.log(name); // "Block"
}
console.log(name); // "Function"
}
greet();
console.log(name); // "Global"
Common Closure Pitfalls and Solutions
// Pitfall 1: Unintended closure in event handlers
function attachListeners() {
const buttons = document.querySelectorAll('button');
// ❌ Wrong - all buttons will alert the last index
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
alert('Button ' + i + ' clicked');
});
}
// ✅ Correct - use let
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
alert('Button ' + i + ' clicked');
});
}
}
// Pitfall 2: Memory leaks with closures
function createHeavyFunction() {
const heavyData = new Array(1000000).fill('data');
return function() {
// Even if we only use one property, the entire heavyData is retained
return heavyData.length;
};
}
// Better approach
function createLightFunction() {
const heavyData = new Array(1000000).fill('data');
const length = heavyData.length; // Extract what we need
return function() {
return length; // Only the number is retained, not the array
};
}
// Pitfall 3: Modifying closed-over variables
function createFunctions() {
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs.push(function() {
return i; // Each function closes over the same 'i' binding
});
}
return funcs;
}
const functions = createFunctions();
console.log(functions[0]()); // 0
console.log(functions[1]()); // 1
console.log(functions[2]()); // 2
Performance Considerations
// Be mindful of closure creation in hot paths
// ❌ Creates new function on every call
function processArray(arr) {
return arr.map(function(item) {
return item * 2;
});
}
// ✅ Better - define function once
const double = item => item * 2;
function processArrayOptimized(arr) {
return arr.map(double);
}
// When closures are worth it
function createValidator(rules) {
// Complex validation setup happens once
const compiledRules = compileRules(rules);
// Return lightweight validator function
return function validate(value) {
return compiledRules.every(rule => rule(value));
};
}
Next Chapter: Chapter 9: The Hoisting Phenomenon
Previous Chapter: Chapter 7: The Object Kingdom Chronicles
Table of Contents: JavaScript Guide