5.6-Scope-Closures
5.6 Scope and Closures
Overview
Understanding scope and closures is fundamental to mastering JavaScript. Scope determines where variables are accessible, while closures allow functions to remember and access variables from their parent scope even after the parent has finished executing.
Table of Contents
- ā¢What is Scope?
- ā¢Types of Scope
- ā¢Variable Declarations and Scope
- ā¢Lexical Scope
- ā¢Scope Chain
- ā¢What are Closures?
- ā¢Practical Closure Examples
- ā¢Closure Use Cases
- ā¢Common Closure Pitfalls
- ā¢Memory Considerations
- ā¢Best Practices
What is Scope?
Scope is the context in which variables and expressions are accessible.
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā GLOBAL SCOPE ā
ā Variables accessible everywhere ā
ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā FUNCTION SCOPE ā ā
ā ā Variables accessible within the function ā ā
ā ā ā ā
ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā ā
ā ā ā BLOCK SCOPE ā ā ā
ā ā ā Variables in {}, if, for, while ā ā ā
ā ā ā (only let and const) ā ā ā
ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā ā
ā ā ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Basic Example
const globalVar = "I'm global";
function exampleFunction() {
const functionVar = "I'm function-scoped";
if (true) {
const blockVar = "I'm block-scoped";
console.log(globalVar); // ā
Accessible
console.log(functionVar); // ā
Accessible
console.log(blockVar); // ā
Accessible
}
console.log(blockVar); // ā ReferenceError
}
console.log(functionVar); // ā ReferenceError
Types of Scope
1. Global Scope
Variables declared outside any function or block.
// Global scope
var globalVar = 'global var';
let globalLet = 'global let';
const globalConst = 'global const';
// These are accessible everywhere
function accessGlobals() {
console.log(globalVar); // ā
console.log(globalLet); // ā
console.log(globalConst); // ā
}
2. Function Scope
Variables declared inside a function are only accessible within that function.
function myFunction() {
var functionVar = "I'm only in this function";
let functionLet = 'Me too';
const functionConst = 'Same here';
console.log(functionVar); // ā
Accessible
}
console.log(functionVar); // ā ReferenceError
3. Block Scope
let and const are block-scoped; var is not.
if (true) {
var varVariable = 'var is NOT block-scoped';
let letVariable = 'let IS block-scoped';
const constVariable = 'const IS block-scoped';
}
console.log(varVariable); // ā
"var is NOT block-scoped"
console.log(letVariable); // ā ReferenceError
console.log(constVariable); // ā ReferenceError
Block Scope in Loops
// var - shared across iterations
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log('var:', i), 100);
}
// Output: 3, 3, 3 (all see final value)
// let - unique per iteration
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log('let:', j), 100);
}
// Output: 0, 1, 2 (each has own scope)
Variable Declarations and Scope
| Declaration | Scope | Hoisting | Re-declaration |
|---|---|---|---|
var | Function | Yes (undefined) | Yes |
let | Block | No (TDZ) | No |
const | Block | No (TDZ) | No |
Hoisting Behavior
// var hoisting
console.log(x); // undefined (hoisted)
var x = 5;
// let/const - Temporal Dead Zone (TDZ)
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;
Temporal Dead Zone (TDZ)
// TDZ starts at block entry
{
// TDZ for 'value' starts here
console.log(value); // ReferenceError
let value = 'initialized'; // TDZ ends
}
Lexical Scope
Scope is determined by where functions are written (defined), not where they are called.
const name = 'Global';
function outer() {
const name = 'Outer';
function inner() {
// 'inner' looks up scope chain to find 'name'
console.log(name); // "Outer" (lexical parent)
}
return inner;
}
const innerFn = outer();
innerFn(); // "Outer" - uses scope where defined
Visual Representation
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Global Scope ā
ā āāā outer() defined here ā
ā āāā inner() defined here (lexical child) ā
ā āāā Looks up to find 'name' ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Scope Chain
When accessing a variable, JavaScript searches up the scope chain.
const a = 'global a';
function outer() {
const b = 'outer b';
function middle() {
const c = 'middle c';
function inner() {
const d = 'inner d';
// Scope chain lookup:
console.log(d); // Found in inner scope
console.log(c); // Found in middle scope
console.log(b); // Found in outer scope
console.log(a); // Found in global scope
}
inner();
}
middle();
}
outer();
Scope Chain Diagram
Variable lookup: a
ā
ā¼
āāāāāāāāāāāāāāāāāāā
ā inner scope ā Not found ā go up
ā d = "inner d" ā
āāāāāāāāāā¬āāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāā
ā middle scope ā Not found ā go up
ā c = "middle c" ā
āāāāāāāāāā¬āāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāā
ā outer scope ā Not found ā go up
ā b = "outer b" ā
āāāāāāāāāā¬āāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāā
ā global scope ā Found! ā
ā a = "global a" ā
āāāāāāāāāāāāāāāāāāā
What are Closures?
A closure is a function that remembers and can access its lexical scope even when executed outside that scope.
Simple Closure
function createCounter() {
let count = 0; // Private variable
return function () {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// 'count' persists between calls!
Closure Visualization
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā createCounter() called ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā count = 0 ā ā
ā ā ā ā
ā ā returned function āāāāāāāāāāāāāāāāāā ā ā
ā ā ā ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā ā
ā createCounter() finished ā Closure ā
ā (but scope is NOT garbage collected) ā (remembers) ā
ā ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā counter() ā
ā Still has access to count! ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Practical Closure Examples
1. Data Privacy (Encapsulation)
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
return balance;
}
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return balance;
}
return 'Insufficient funds';
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(100);
console.log(account.getBalance()); // 100
account.deposit(50);
console.log(account.getBalance()); // 150
account.withdraw(30);
console.log(account.getBalance()); // 120
// Cannot access balance directly!
console.log(account.balance); // undefined
2. Function Factory
function createMultiplier(multiplier) {
return function (number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const quadruple = createMultiplier(4);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(quadruple(5)); // 20
3. Memoization
function memoize(fn) {
const cache = {}; // Closure over cache
return function (...args) {
const key = JSON.stringify(args);
if (cache[key] !== undefined) {
console.log('Cache hit!');
return cache[key];
}
console.log('Computing...');
const result = fn(...args);
cache[key] = result;
return result;
};
}
const expensiveOperation = memoize((n) => {
// Simulate expensive computation
let result = 0;
for (let i = 0; i < n * 1000000; i++) {
result += i;
}
return result;
});
expensiveOperation(10); // "Computing..." (slow)
expensiveOperation(10); // "Cache hit!" (instant)
4. Event Handlers with State
function createToggle(element) {
let isOn = false; // Private state
element.addEventListener('click', function () {
isOn = !isOn;
element.textContent = isOn ? 'ON' : 'OFF';
element.classList.toggle('active', isOn);
});
}
// createToggle(buttonElement);
Closure Use Cases
1. Module Pattern
const Calculator = (function () {
// Private
let history = [];
function addToHistory(operation) {
history.push(operation);
}
// Public API
return {
add(a, b) {
const result = a + b;
addToHistory(`${a} + ${b} = ${result}`);
return result;
},
subtract(a, b) {
const result = a - b;
addToHistory(`${a} - ${b} = ${result}`);
return result;
},
getHistory() {
return [...history]; // Return copy
},
};
})();
Calculator.add(5, 3); // 8
Calculator.subtract(10, 4); // 6
console.log(Calculator.getHistory());
// ["5 + 3 = 8", "10 - 4 = 6"]
2. Partial Application
function partial(fn, ...presetArgs) {
return function (...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
function greet(greeting, name, punctuation) {
return `${greeting}, ${name}${punctuation}`;
}
const sayHello = partial(greet, 'Hello');
console.log(sayHello('Alice', '!')); // "Hello, Alice!"
3. Once Function
function once(fn) {
let called = false;
let result;
return function (...args) {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}
const initialize = once(() => {
console.log('Initializing...');
return 'Initialized!';
});
console.log(initialize()); // "Initializing..." then "Initialized!"
console.log(initialize()); // Just "Initialized!" (no re-run)
4. Debounce Function
function debounce(fn, delay) {
let timeoutId; // Closure maintains timer
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
const search = debounce((query) => {
console.log('Searching for:', query);
}, 300);
// Rapid calls
search('a');
search('ab');
search('abc');
// Only "abc" will be searched (after 300ms)
5. Rate Limiter
function rateLimit(fn, limit) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
return fn(...args);
}
};
}
const limitedApi = rateLimit(() => {
console.log('API called at', Date.now());
}, 1000);
Common Closure Pitfalls
1. Loop Variable Issue (var)
// ā Problem with var
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 3, 3, 3
}, 100);
}
// ā
Solution 1: Use let
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 0, 1, 2
}, 100);
}
// ā
Solution 2: IIFE (legacy)
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(function () {
console.log(j); // 0, 1, 2
}, 100);
})(i);
}
2. Accidental Closure Over Reference
// ā Problem: closure over object reference
function createHandlers() {
const handlers = [];
const obj = { value: 0 };
for (let i = 0; i < 3; i++) {
obj.value = i;
handlers.push(() => console.log(obj.value));
}
return handlers;
}
const handlers = createHandlers();
handlers[0](); // 2 (not 0!)
handlers[1](); // 2
handlers[2](); // 2
// ā
Solution: Create new object each iteration
function createHandlersFixed() {
const handlers = [];
for (let i = 0; i < 3; i++) {
handlers.push(() => console.log(i));
}
return handlers;
}
3. Closure Over Wrong 'this'
// ā Problem
const obj = {
name: 'MyObject',
createLogger() {
return function () {
console.log(this.name); // undefined
};
},
};
// ā
Solution 1: Arrow function
const obj2 = {
name: 'MyObject',
createLogger() {
return () => {
console.log(this.name); // "MyObject"
};
},
};
// ā
Solution 2: Capture 'this'
const obj3 = {
name: 'MyObject',
createLogger() {
const self = this;
return function () {
console.log(self.name); // "MyObject"
};
},
};
Memory Considerations
Closures can cause memory leaks if not managed properly.
Memory Leak Example
// ā Potential memory leak
function createHugeArray() {
const hugeData = new Array(1000000).fill('data');
return function () {
// Entire hugeData is kept in memory
// even though we only use length
console.log(hugeData.length);
};
}
// ā
Fixed: Only capture what you need
function createHugeArrayFixed() {
const hugeData = new Array(1000000).fill('data');
const length = hugeData.length; // Capture only needed value
return function () {
console.log(length);
// hugeData can be garbage collected
};
}
Event Listener Cleanup
function setup(element) {
const data = {
/* large object */
};
function handler() {
console.log(data);
}
element.addEventListener('click', handler);
// Return cleanup function
return function cleanup() {
element.removeEventListener('click', handler);
};
}
const cleanup = setup(element);
// Later:
cleanup(); // Removes listener, allows garbage collection
Best Practices
1. Minimize Closure Scope
// ā Capturing entire large object
function process(largeObject) {
const data = largeObject;
return () => data.value;
}
// ā
Capture only what's needed
function processOptimized(largeObject) {
const value = largeObject.value;
return () => value;
}
2. Use let Instead of var in Loops
// ā
Always use let in loops with async operations
for (let i = 0; i < 10; i++) {
setTimeout(() => console.log(i), 100);
}
3. Clean Up When Done
function createResource() {
const resource = heavyResource();
return {
use() {
/* use resource */
},
dispose() {
resource.cleanup();
// Help garbage collector
},
};
}
4. Document Closure Behavior
/**
* Creates a counter with private state.
* @returns {Object} Counter with increment, decrement, getCount methods
* @description Uses closure to encapsulate count variable
*/
function createCounter() {
let count = 0; // Private, persists via closure
// ...
}
Summary
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā SCOPE AND CLOSURES ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā SCOPE TYPES: ā
ā ⢠Global - Accessible everywhere ā
ā ⢠Function - Accessible within function ā
ā ⢠Block - let/const within {} ā
ā ā
ā DECLARATIONS: ā
ā ⢠var - Function-scoped, hoisted ā
ā ⢠let - Block-scoped, TDZ ā
ā ⢠const - Block-scoped, TDZ, immutable binding ā
ā ā
ā CLOSURES: ā
ā ⢠Function + its lexical environment ā
ā ⢠Remembers variables from parent scope ā
ā ⢠Persists even after parent function returns ā
ā ā
ā USE CASES: ā
ā ⢠Data privacy/encapsulation ā
ā ⢠Function factories ā
ā ⢠Memoization/caching ā
ā ⢠Partial application ā
ā ⢠Module pattern ā
ā ⢠Event handlers with state ā
ā ā
ā PITFALLS: ā
ā ⢠var in loops with async ā
ā ⢠Closure over references (not values) ā
ā ⢠Memory leaks (unused closures) ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Next Steps
- ā¢Practice with the examples in
examples.js - ā¢Complete the exercises in
exercises.js - ā¢Learn about higher-order functions
- ā¢Explore the module pattern in depth