We are still cooking the magic in the way!
Scope & Closures
Understanding Scope in JavaScript
Scope is one of the most fundamental concepts in JavaScript. It determines where variables and functions are accessible in your code. Understanding scope is essential for writing bug-free programs, managing memory efficiently, and organizing code in a clean and predictable way. Every variable you declare lives in a specific scope, and JavaScript follows precise rules to determine which variables are visible from any given point in your code.
There are three main types of scope in JavaScript: global scope, function scope, and block scope. Each type has distinct characteristics that affect how your variables behave. Mastering these concepts will help you avoid common pitfalls like accidental variable overwrites, unexpected undefined values, and memory leaks. In this lesson, we will explore each type of scope in depth, understand how the scope chain works, and then dive into closures -- one of the most powerful patterns in JavaScript.
Global Scope
The global scope is the outermost scope in your JavaScript program. Variables declared outside of any function or block are in the global scope. These variables are accessible from anywhere in your code -- inside functions, inside loops, inside conditionals, and from any file that runs in the same execution context. In a browser environment, global variables become properties of the window object. In Node.js, they become properties of the global object.
Example: Global Scope
// Variables declared at the top level are in global scope
var globalVar = 'I am global';
let globalLet = 'I am also global';
const globalConst = 'I am global too';
function showGlobals() {
// All global variables are accessible inside functions
console.log(globalVar); // I am global
console.log(globalLet); // I am also global
console.log(globalConst); // I am global too
}
showGlobals();
// In a browser, var creates a property on window
var browserVar = 'Hello';
// console.log(window.browserVar); // 'Hello' (in browser)
// let and const do NOT create window properties
let browserLet = 'World';
// console.log(window.browserLet); // undefined (in browser)
Example: Global Variable Naming Collision
// File 1: Sets a global variable
var config = { theme: 'dark', language: 'en' };
// File 2: Accidentally overwrites the same global variable
var config = { apiUrl: 'https://api.example.com' };
// The original config is completely lost
console.log(config); // { apiUrl: 'https://api.example.com' }
// config.theme is now undefined -- this could break your app
Function Scope
Function scope means that variables declared inside a function are only accessible within that function. They cannot be accessed from outside the function. This applies to variables declared with var, let, and const. Function scope creates a boundary that encapsulates variables, preventing them from leaking out into the surrounding scope. Each function call creates a new scope, so variables with the same name in different functions do not interfere with each other.
Example: Function Scope
function calculateTotal(price, taxRate) {
// These variables only exist inside this function
var subtotal = price;
let tax = price * taxRate;
const total = subtotal + tax;
console.log(total); // accessible here
return total;
}
calculateTotal(100, 0.1); // 110
// These will all throw ReferenceError
// console.log(subtotal); // ReferenceError: subtotal is not defined
// console.log(tax); // ReferenceError: tax is not defined
// console.log(total); // ReferenceError: total is not defined
// Each function call creates its own scope
function counter() {
var count = 0;
count++;
console.log(count);
}
counter(); // 1
counter(); // 1 (not 2 -- a new 'count' is created each time)
counter(); // 1
Example: Nested Function Scope
function outer() {
var outerVar = 'I am from outer';
function inner() {
var innerVar = 'I am from inner';
console.log(outerVar); // accessible -- inner can see outer's variables
console.log(innerVar); // accessible -- inner's own variable
}
inner();
console.log(outerVar); // accessible
// console.log(innerVar); // ReferenceError -- outer cannot see inner's variables
}
outer();
Block Scope with let and const
Block scope was introduced in ES6 with the let and const keywords. A block is any code enclosed in curly braces {}, such as the body of an if statement, a for loop, a while loop, or even a standalone block. Variables declared with let or const inside a block are only accessible within that block. This is different from var, which is function-scoped and ignores block boundaries.
Example: Block Scope vs Function Scope
// var is function-scoped -- it ignores block boundaries
if (true) {
var varVariable = 'I leak out of blocks!';
let letVariable = 'I stay inside the block';
const constVariable = 'I also stay inside';
}
console.log(varVariable); // 'I leak out of blocks!' (var ignores blocks)
// console.log(letVariable); // ReferenceError
// console.log(constVariable); // ReferenceError
// Block scope in for loops
for (let i = 0; i < 3; i++) {
// 'i' only exists inside this loop
console.log(i); // 0, 1, 2
}
// console.log(i); // ReferenceError: i is not defined
// Compare with var in a for loop
for (var j = 0; j < 3; j++) {
console.log(j); // 0, 1, 2
}
console.log(j); // 3 (var leaks out of the loop!)
// Standalone blocks create scope too
{
let blockScoped = 'only here';
console.log(blockScoped); // 'only here'
}
// console.log(blockScoped); // ReferenceError
var and let/const is one of the primary reasons modern JavaScript strongly favors let and const over var. Block scoping makes code more predictable because variables are confined to the smallest possible scope. Always use const by default, switch to let only when you need to reassign, and avoid var entirely in modern code.Example: Practical Block Scope Benefits
// Block scope prevents variable leaking in conditionals
function processUser(user) {
if (user.isAdmin) {
const adminPanel = 'Full Access';
let permissions = ['read', 'write', 'delete'];
console.log(adminPanel, permissions);
}
if (user.isEditor) {
// This is a DIFFERENT 'permissions' variable -- no conflict
let permissions = ['read', 'write'];
console.log(permissions);
}
// Neither adminPanel nor permissions leak out here
// This keeps the function body clean and predictable
}
// Block scope in switch statements
function getDayType(day) {
switch (day) {
case 'Saturday':
case 'Sunday': {
const type = 'weekend';
return type;
}
default: {
const type = 'weekday'; // same name, different block -- no conflict
return type;
}
}
}
Lexical Scoping
JavaScript uses lexical scoping (also called static scoping), which means that the scope of a variable is determined by its position in the source code, not by the order in which functions are called at runtime. When a function is defined, it permanently remembers the scope in which it was created. This is the foundation upon which closures are built.
In lexical scoping, inner functions have access to variables defined in their outer functions. This access is determined at the time the code is written (lexical time), not at the time the code is executed. This is why it is called lexical scoping -- it follows the lexical structure of the code.
Example: Lexical Scoping
const outerValue = 'outer';
function outerFunction() {
const middleValue = 'middle';
function middleFunction() {
const innerValue = 'inner';
function innerFunction() {
// This function can access ALL outer scopes
console.log(outerValue); // 'outer' (global scope)
console.log(middleValue); // 'middle' (outerFunction scope)
console.log(innerValue); // 'inner' (middleFunction scope)
}
innerFunction();
}
middleFunction();
}
outerFunction();
// Lexical scope is determined by WHERE a function is defined
// NOT by where or how it is called
let greeting = 'Hello';
function greet() {
console.log(greeting); // looks up greeting in its lexical scope
}
function changeAndGreet() {
let greeting = 'Howdy'; // this is a different variable in a different scope
greet(); // still prints 'Hello' -- greet's lexical scope has the global greeting
}
changeAndGreet(); // Hello (not Howdy)
The Scope Chain
When JavaScript encounters a variable, it searches for it starting from the current scope and moving outward through each enclosing scope until it reaches the global scope. This chain of scopes is called the scope chain. If the variable is found in any scope along the chain, that value is used. If the variable is not found even in the global scope, a ReferenceError is thrown. The scope chain always moves outward -- inner scopes can access outer scopes, but not the other way around.
Example: Scope Chain in Action
const global = 'global';
function levelOne() {
const one = 'level one';
function levelTwo() {
const two = 'level two';
function levelThree() {
const three = 'level three';
// Scope chain: levelThree -> levelTwo -> levelOne -> global
console.log(three); // found in current scope
console.log(two); // found one level up
console.log(one); // found two levels up
console.log(global); // found in global scope
}
levelThree();
}
levelTwo();
}
levelOne();
// Variable shadowing -- inner scope variable hides outer variable
let color = 'blue';
function paintHouse() {
let color = 'red'; // shadows the outer 'color'
function paintRoom() {
let color = 'green'; // shadows paintHouse's 'color'
console.log(color); // 'green' (found in current scope first)
}
paintRoom();
console.log(color); // 'red' (paintHouse's own color)
}
paintHouse();
console.log(color); // 'blue' (global color unchanged)
What Are Closures?
A closure is created when a function retains access to variables from its outer (enclosing) scope, even after the outer function has finished executing. In other words, a closure is a function bundled together with its lexical environment. Every function in JavaScript creates a closure, but the term is most commonly used when an inner function is returned from an outer function and continues to reference the outer function's variables.
Closures are not a special syntax or a separate feature you enable. They are a natural consequence of lexical scoping combined with the fact that functions are first-class values in JavaScript (meaning they can be passed around, returned, and stored just like any other value). Understanding closures unlocks powerful programming patterns including data privacy, function factories, partial application, and the module pattern.
Example: Your First Closure
function createGreeter(greeting) {
// 'greeting' is in the outer function's scope
return function(name) {
// This inner function has access to 'greeting'
// even after createGreeter has finished executing
console.log(greeting + ', ' + name + '!');
};
}
const sayHello = createGreeter('Hello');
const sayHi = createGreeter('Hi');
// createGreeter has already returned, but the inner functions
// still have access to their respective 'greeting' values
sayHello('Alice'); // Hello, Alice!
sayHello('Bob'); // Hello, Bob!
sayHi('Charlie'); // Hi, Charlie!
// Each closure captures its OWN copy of the outer variables
// sayHello's greeting is 'Hello', sayHi's greeting is 'Hi'
Closure Examples: Counter
One of the classic examples of closures is building a counter. The counter variable is enclosed in the outer function's scope, and the returned function (or functions) can access and modify it. The counter variable is not accessible from outside -- it is effectively private. Each call to the factory function creates a new, independent counter with its own enclosed state.
Example: Counter with Closures
function createCounter(initialValue = 0) {
let count = initialValue; // this variable is enclosed
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
},
reset: function() {
count = initialValue;
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.increment()); // 3
console.log(counter.decrement()); // 2
console.log(counter.getCount()); // 2
console.log(counter.reset()); // 0
// Each counter is independent
const counterA = createCounter(10);
const counterB = createCounter(100);
console.log(counterA.increment()); // 11
console.log(counterB.increment()); // 101
console.log(counterA.getCount()); // 11 (not affected by counterB)
// 'count' is not accessible from outside
// console.log(counter.count); // undefined -- it is private
Closure Examples: Private Variables
Closures provide the only way to create truly private variables in JavaScript (prior to the recent addition of private class fields with #). By enclosing variables inside a function and only exposing specific methods to interact with them, you can control exactly how data is accessed and modified. This pattern is fundamental to writing secure and maintainable code.
Example: Private Variables with Closures
function createBankAccount(initialBalance) {
let balance = initialBalance; // private variable
const transactions = []; // private array
return {
deposit: function(amount) {
if (amount <= 0) {
console.log('Deposit amount must be positive');
return;
}
balance += amount;
transactions.push({ type: 'deposit', amount, date: new Date() });
console.log('Deposited: $' + amount + '. Balance: $' + balance);
},
withdraw: function(amount) {
if (amount <= 0) {
console.log('Withdrawal amount must be positive');
return;
}
if (amount > balance) {
console.log('Insufficient funds. Balance: $' + balance);
return;
}
balance -= amount;
transactions.push({ type: 'withdrawal', amount, date: new Date() });
console.log('Withdrawn: $' + amount + '. Balance: $' + balance);
},
getBalance: function() {
return balance;
},
getTransactionCount: function() {
return transactions.length;
}
};
}
const account = createBankAccount(1000);
account.deposit(500); // Deposited: $500. Balance: $1500
account.withdraw(200); // Withdrawn: $200. Balance: $1300
console.log(account.getBalance()); // 1300
console.log(account.getTransactionCount()); // 2
// Cannot directly access or modify private data
// account.balance = 999999; // This creates a new property, does NOT change the enclosed balance
console.log(account.getBalance()); // still 1300
Example: Private State for Validation
function createValidator(rules) {
// rules is captured by closure and cannot be modified externally
const errors = [];
return {
validate: function(data) {
errors.length = 0; // clear previous errors
for (const rule of rules) {
if (!rule.test(data[rule.field])) {
errors.push(rule.message);
}
}
return errors.length === 0;
},
getErrors: function() {
return [...errors]; // return a copy to prevent external modification
}
};
}
const userValidator = createValidator([
{ field: 'name', test: val => val && val.length >= 2, message: 'Name must be at least 2 characters' },
{ field: 'email', test: val => val && val.includes('@'), message: 'Email must contain @' },
{ field: 'age', test: val => val && val >= 18, message: 'Must be 18 or older' }
]);
console.log(userValidator.validate({ name: 'A', email: 'bad', age: 15 })); // false
console.log(userValidator.getErrors());
// ['Name must be at least 2 characters', 'Email must contain @', 'Must be 18 or older']
console.log(userValidator.validate({ name: 'Alice', email: 'alice@mail.com', age: 25 })); // true
console.log(userValidator.getErrors()); // []
Closures in Loops: The Classic Pitfall
One of the most famous JavaScript interview questions and debugging challenges involves closures inside loops. When you use var in a loop and create closures that reference the loop variable, all closures share the same variable because var is function-scoped, not block-scoped. By the time the closures execute, the loop has already finished, and the variable holds its final value. This is one of the most important reasons to use let instead of var.
Example: The Classic Loop Closure Problem
// THE PROBLEM: var is function-scoped
function createButtonHandlersVar() {
var handlers = [];
for (var i = 0; i < 5; i++) {
handlers.push(function() {
console.log('Button ' + i + ' clicked');
});
}
return handlers;
}
var buttons = createButtonHandlersVar();
buttons[0](); // Button 5 clicked (expected: Button 0 clicked)
buttons[1](); // Button 5 clicked (expected: Button 1 clicked)
buttons[2](); // Button 5 clicked (expected: Button 2 clicked)
// All handlers print 5 because they all share the same 'i' variable
// By the time any handler runs, the loop is done and i === 5
Example: Three Solutions to the Loop Closure Problem
// SOLUTION 1: Use let (modern and preferred)
function createButtonHandlersLet() {
const handlers = [];
for (let i = 0; i < 5; i++) {
// let creates a new 'i' for each iteration
handlers.push(function() {
console.log('Button ' + i + ' clicked');
});
}
return handlers;
}
const buttonsLet = createButtonHandlersLet();
buttonsLet[0](); // Button 0 clicked
buttonsLet[1](); // Button 1 clicked
buttonsLet[2](); // Button 2 clicked
// SOLUTION 2: Use an IIFE (pre-ES6 approach)
function createButtonHandlersIIFE() {
var handlers = [];
for (var i = 0; i < 5; i++) {
handlers.push((function(capturedI) {
return function() {
console.log('Button ' + capturedI + ' clicked');
};
})(i)); // immediately invoke with current i value
}
return handlers;
}
// SOLUTION 3: Use forEach (avoids manual loop variable)
function createButtonHandlersForEach() {
return [0, 1, 2, 3, 4].map(function(i) {
return function() {
console.log('Button ' + i + ' clicked');
};
});
}
for loops. Any situation where you create closures inside a loop using var will have the same problem. This includes while loops, do...while loops, and even array methods if you somehow use var inside them. The solution is always the same: use let to create a new variable binding for each iteration.The Module Pattern with Closures
Before ES6 modules, the module pattern was the standard way to organize JavaScript code and create encapsulated, reusable components. It uses an Immediately Invoked Function Expression (IIFE) combined with closures to create a private scope, and then returns an object containing only the methods and properties that should be publicly accessible. This pattern is still widely used and important to understand.
Example: Module Pattern
// Module pattern using IIFE and closures
const UserModule = (function() {
// Private variables and functions
const users = [];
let nextId = 1;
function findUserIndex(id) {
return users.findIndex(user => user.id === id);
}
function validateUser(userData) {
if (!userData.name || userData.name.trim() === '') {
throw new Error('Name is required');
}
if (!userData.email || !userData.email.includes('@')) {
throw new Error('Valid email is required');
}
return true;
}
// Public API (returned object)
return {
addUser: function(userData) {
validateUser(userData);
const user = {
id: nextId++,
name: userData.name,
email: userData.email,
createdAt: new Date()
};
users.push(user);
return user;
},
removeUser: function(id) {
const index = findUserIndex(id);
if (index === -1) {
throw new Error('User not found');
}
return users.splice(index, 1)[0];
},
getUser: function(id) {
const index = findUserIndex(id);
if (index === -1) return null;
return { ...users[index] }; // return a copy
},
getAllUsers: function() {
return users.map(user => ({ ...user })); // return copies
},
getUserCount: function() {
return users.length;
}
};
})();
// Use the module
const alice = UserModule.addUser({ name: 'Alice', email: 'alice@mail.com' });
const bob = UserModule.addUser({ name: 'Bob', email: 'bob@mail.com' });
console.log(UserModule.getUserCount()); // 2
console.log(UserModule.getUser(1)); // { id: 1, name: 'Alice', ... }
console.log(UserModule.getAllUsers()); // [{ id: 1, ... }, { id: 2, ... }]
// Private internals are completely hidden
// console.log(UserModule.users); // undefined
// console.log(UserModule.nextId); // undefined
// console.log(UserModule.findUserIndex); // undefined
// console.log(UserModule.validateUser); // undefined
Example: Configurable Module Pattern
// Module that accepts configuration
const Logger = (function() {
let logLevel = 'info';
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
const logs = [];
function shouldLog(level) {
return levels[level] >= levels[logLevel];
}
function formatMessage(level, message) {
const timestamp = new Date().toISOString();
return '[' + timestamp + '] [' + level.toUpperCase() + '] ' + message;
}
return {
setLevel: function(level) {
if (levels[level] === undefined) {
throw new Error('Invalid log level: ' + level);
}
logLevel = level;
},
debug: function(msg) {
if (shouldLog('debug')) {
const formatted = formatMessage('debug', msg);
logs.push(formatted);
console.log(formatted);
}
},
info: function(msg) {
if (shouldLog('info')) {
const formatted = formatMessage('info', msg);
logs.push(formatted);
console.log(formatted);
}
},
warn: function(msg) {
if (shouldLog('warn')) {
const formatted = formatMessage('warn', msg);
logs.push(formatted);
console.warn(formatted);
}
},
error: function(msg) {
if (shouldLog('error')) {
const formatted = formatMessage('error', msg);
logs.push(formatted);
console.error(formatted);
}
},
getLogHistory: function() {
return [...logs];
}
};
})();
Logger.setLevel('warn');
Logger.debug('This will not appear'); // below threshold
Logger.info('This will not appear'); // below threshold
Logger.warn('Low disk space'); // will appear
Logger.error('Connection failed'); // will appear
Memory Implications of Closures
Because closures retain references to their outer scope variables, those variables cannot be garbage collected as long as the closure exists. This is usually a feature, not a bug -- it is what allows closures to work. However, it can become a problem if you create closures that capture large data structures unintentionally, or if closures persist longer than necessary. Understanding the memory implications helps you write efficient code and avoid memory leaks.
Example: Memory Considerations
// Potential memory issue: capturing large data
function processLargeData() {
const hugeArray = new Array(1000000).fill('data'); // large data
return function getLength() {
// This closure keeps hugeArray alive in memory
// even though it only needs the length
return hugeArray.length;
};
}
const getLen = processLargeData();
// hugeArray cannot be garbage collected because getLen references it
// BETTER: Capture only what you need
function processLargeDataBetter() {
const hugeArray = new Array(1000000).fill('data');
const length = hugeArray.length; // extract what you need
return function getLength() {
return length; // only captures the number, not the array
};
// hugeArray can be garbage collected after processLargeDataBetter returns
}
// Cleaning up closures
function createEventHandler(element) {
let clickCount = 0;
function handler() {
clickCount++;
console.log('Clicks: ' + clickCount);
}
element.addEventListener('click', handler);
// Return a cleanup function
return function cleanup() {
element.removeEventListener('click', handler);
// Now handler can be garbage collected
// and clickCount with it
};
}
// Usage:
// const cleanup = createEventHandler(myButton);
// Later, when no longer needed:
// cleanup();
Practical Use Cases
Closures are not just theoretical concepts -- they are used everywhere in real-world JavaScript development. Let us explore the most common and practical use cases that you will encounter and use in your own projects.
Event Handlers with State
Closures allow event handlers to maintain state between invocations without using global variables. Each handler can have its own private state that persists across multiple events.
Example: Event Handlers with Closure State
// Debounce function -- one of the most common closure patterns
function debounce(fn, delay) {
let timeoutId = null; // private to this closure
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Usage
const handleSearch = debounce(function(query) {
console.log('Searching for: ' + query);
}, 300);
// Rapid calls -- only the last one executes
handleSearch('j');
handleSearch('ja');
handleSearch('jav');
handleSearch('java');
handleSearch('javascript');
// Only logs: 'Searching for: javascript' (after 300ms)
// Throttle function -- another classic closure pattern
function throttle(fn, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
const handleScroll = throttle(function() {
console.log('Scroll position: ' + window.scrollY);
}, 200);
Data Privacy and Encapsulation
Closures provide true data privacy, something that plain objects cannot offer. By hiding internal state behind closure boundaries, you create APIs that can only be used through the methods you explicitly expose.
Example: Data Privacy Patterns
// Rate limiter with private state
function createRateLimiter(maxRequests, timeWindow) {
const requests = []; // private request timestamps
return function isAllowed() {
const now = Date.now();
// Remove expired timestamps
while (requests.length > 0 && requests[0] <= now - timeWindow) {
requests.shift();
}
if (requests.length < maxRequests) {
requests.push(now);
return true;
}
return false;
};
}
const limiter = createRateLimiter(3, 1000); // 3 requests per second
console.log(limiter()); // true
console.log(limiter()); // true
console.log(limiter()); // true
console.log(limiter()); // false (limit reached)
// Cache with private data
function createCache(maxSize = 100) {
const cache = new Map(); // private
return {
get: function(key) {
return cache.get(key);
},
set: function(key, value) {
if (cache.size >= maxSize) {
// Remove the oldest entry
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(key, value);
},
has: function(key) {
return cache.has(key);
},
size: function() {
return cache.size;
},
clear: function() {
cache.clear();
}
};
}
const myCache = createCache(3);
myCache.set('a', 1);
myCache.set('b', 2);
myCache.set('c', 3);
myCache.set('d', 4); // 'a' is evicted
console.log(myCache.has('a')); // false
console.log(myCache.has('d')); // true
Function Factories
Function factories use closures to create specialized functions from a general template. The factory function takes configuration parameters and returns a new function that has those parameters permanently baked in through closure. This is an incredibly powerful and clean pattern for creating reusable, configurable behavior.
Example: Function Factories
// Multiplier factory
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const toPercent = createMultiplier(100);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(toPercent(0.75)); // 75
// Formatter factory
function createFormatter(currency, locale) {
const formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
});
return function(amount) {
return formatter.format(amount);
};
}
const formatUSD = createFormatter('USD', 'en-US');
const formatEUR = createFormatter('EUR', 'de-DE');
const formatJPY = createFormatter('JPY', 'ja-JP');
console.log(formatUSD(1234.56)); // $1,234.56
console.log(formatEUR(1234.56)); // 1.234,56 EUR
console.log(formatJPY(1234)); // JP 1,234
// URL builder factory
function createApiClient(baseUrl) {
return {
get: function(endpoint) {
const url = baseUrl + endpoint;
console.log('GET ' + url);
return fetch(url);
},
post: function(endpoint, data) {
const url = baseUrl + endpoint;
console.log('POST ' + url);
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
};
}
const api = createApiClient('https://api.example.com');
// api.get('/users'); // GET https://api.example.com/users
// api.post('/users', data); // POST https://api.example.com/users
Example: Advanced Function Factory -- Memoization
// Memoization: caching expensive function results using closures
function memoize(fn) {
const cache = new Map(); // private to this closure
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Cache hit for:', key);
return cache.get(key);
}
console.log('Computing for:', key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
// Expensive computation example
const fibonacci = memoize(function fib(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(10)); // Computing (first time)... 55
console.log(fibonacci(10)); // Cache hit... 55
console.log(fibonacci(8)); // Cache hit... 21 (was computed as part of fibonacci(10))
// Memoize any pure function
const expensiveCalculation = memoize(function(x, y) {
console.log('Heavy computation...');
let result = 0;
for (let i = 0; i < x * y; i++) {
result += Math.sqrt(i);
}
return result;
});
expensiveCalculation(100, 200); // Heavy computation...
expensiveCalculation(100, 200); // Cache hit (instant)
Closures and Asynchronous Code
Closures play a critical role in asynchronous JavaScript. When you pass a callback to setTimeout, make a fetch request, or set up an event listener, the callback function forms a closure over its surrounding variables. This allows asynchronous code to access data that was available when the callback was created, even though the callback executes much later in time.
Example: Closures in Async Patterns
// Closures preserve state across async operations
function fetchUserData(userId) {
const startTime = Date.now(); // captured by closure
fetch('/api/users/' + userId)
.then(function(response) {
return response.json();
})
.then(function(user) {
const elapsed = Date.now() - startTime; // closure has access to startTime
console.log('Loaded ' + user.name + ' in ' + elapsed + 'ms');
});
}
// Sequential async with closures
function processItemsSequentially(items) {
let index = 0; // shared state via closure
function processNext() {
if (index >= items.length) {
console.log('All items processed');
return;
}
const currentItem = items[index]; // captured for this iteration
index++;
setTimeout(function() {
console.log('Processing: ' + currentItem);
processNext(); // recursive call
}, 1000);
}
processNext();
}
processItemsSequentially(['Apple', 'Banana', 'Cherry']);
// After 1s: Processing: Apple
// After 2s: Processing: Banana
// After 3s: Processing: Cherry
// All items processed
Summary of Key Concepts
Let us review the most important concepts from this lesson:
- Global scope -- Variables accessible everywhere. Minimize their use to avoid collisions and unintended modifications.
- Function scope -- Variables declared inside a function are private to that function. Created fresh on each function call.
- Block scope --
letandconstare confined to the nearest enclosing{}block. Always prefer these overvar. - Lexical scoping -- Scope is determined by where code is written, not where it is called. Inner functions can access outer variables.
- Scope chain -- Variable lookup moves outward from the current scope through each enclosing scope to the global scope.
- Closures -- Functions that retain access to their outer scope variables even after the outer function has returned. They enable data privacy, function factories, and stateful callbacks.
- Loop closures -- Use
letin loops to create a new binding per iteration, avoiding the classicvarclosure pitfall. - Module pattern -- Uses IIFEs and closures to create private state with a public API.
- Memory -- Closures keep referenced variables alive. Capture only what you need and clean up when done.
Practice Exercise
Complete the following challenges to solidify your understanding of scope and closures:
- Write a function called
createMultiplierthat takes a number and returns a function that multiplies any given number by the original number. Test it by creatingdoubleandtriplefunctions and verify they work independently. - Implement a
createStackfunction that returns an object withpush,pop,peek, andsizemethods. The internal array should be completely private and not accessible from outside. Test that you cannot access the internal array directly. - Create a
oncefunction that takes a function as an argument and returns a new function that can only be called once. Subsequent calls should return the result from the first call without re-executing the original function. Use closure to track whether the function has been called. - Write a
createSecretKeeperfunction that takes an initial secret string. It should return an object withgetSecret(requires a password),setSecret(requires the current password), andchangePasswordmethods. All internal state should be private and only accessible through the returned methods. - Implement a
memoizefunction that caches the results of expensive function calls. It should accept any function and return a memoized version. Test it with a recursive Fibonacci function and verify that subsequent calls with the same argument return instantly from the cache.