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
