Closures (The Memory Keeper)
The Mystery of Remembered Values
Look at this code - there's something magical happening:
function createCounter() {
let count = 0; // This variable is "private"
return function() {
count++; // How does this remember 'count'?
console.log(count);
};
}
const counter1 = createCounter();
const counter2 = createCounter();
counter1(); // 1
counter1(); // 2
counter1(); // 3
counter2(); // 1 ← Different counter!
counter2(); // 2The Mystery:
createCounter()finished running- The
countvariable should be gone - But somehow the returned function still remembers
count! - Each counter has its own private
count!
How is this possible? 🤔
The Answer: Closures! 🎉
What is a Closure? (No Jargon)
Simple Definition
A closure is when a function remembers the variables from the place where it was created, even after that place is gone.
Think of it like a backpack that a function carries around. Inside the backpack are all the variables the function needs, and it never loses them!
function outer() {
const message = "Hello!"; // Variable in outer
function inner() {
console.log(message); // inner "remembers" message
}
return inner; // Return the function with its backpack
}
const greet = outer();
// outer() is done running, but...
greet(); // "Hello!" ← Still remembers message! ✨The Key Point
Closures let functions "capture" and "remember" variables from their surrounding context, even after that context no longer exists.
Real-World Analogy
The Time Capsule
Imagine you're creating a time capsule:
Without Closure (Forgotten) ❌
1. You write a letter in your room
"Dear Future Me, remember count = 5"
2. You seal the letter in a box
3. You throw away everything in your room
(including the note about count = 5)
4. Years later, you open the box
The letter says "remember count = 5"
But count is gone! You threw it away! 😱With Closure (Remembered) ✅
1. You write a letter in your room
"Dear Future Me, remember count = 5"
2. You ALSO put the actual note (count = 5)
INSIDE the time capsule
3. You throw away everything in your room
But the capsule still has the count note!
4. Years later, you open the box
The letter says "remember count = 5"
And there it is! Still in the capsule! ✨That's a closure! The function (letter) carries the variables (count note) with it in a "capsule" (closure), so they're never lost.
How Regular Functions Work
Function Without Closure
let globalCount = 0;
function increment() {
globalCount++;
console.log(globalCount);
}
increment(); // 1
increment(); // 2
increment(); // 3What's happening:
Global Memory:
┌─────────────────────┐
│ globalCount = 0 │ ← Everyone can access this
└─────────────────────┘
↓
increment()
↓
Uses globalCountProblems:
- ❌ Anyone can change
globalCount - ❌ No way to have multiple independent counters
- ❌ Global namespace pollution
globalCount = 999; // Oops! Anyone can mess this up!
increment(); // 1000 ← Our counter is broken! 😱How Closures Work
Function With Closure
function createCounter() {
let count = 0; // Private variable
return function() {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3
// Can't access count directly!
console.log(count); // Error: count is not defined ✅What's happening:
1. Call createCounter()
↓
Creates local variable: count = 0
↓
Creates inner function
↓
Inner function "captures" count in closure
↓
Returns inner function with its closure
2. createCounter() finishes
↓
Normally, count would be deleted
↓
BUT! The returned function still references count
↓
So JavaScript keeps count alive in the closure! ✨
3. Call counter()
↓
Function reaches into its closure
↓
Finds count (still there!)
↓
Increments itVisual representation:
counter (function)
├─ Function code: count++; console.log(count)
└─ Closure (backpack):
└─ count = 0 ← Remembered from createCounter()Step-by-Step: Building Your First Closure
Example: Secret Name Storage
Let's build a function that creates a private name storage.
Step 1: The Naive Approach (Without Closure)
let name = "Alice"; // Global variable
function getName() {
return name;
}
function setName(newName) {
name = newName;
}
console.log(getName()); // "Alice"
setName("Bob");
console.log(getName()); // "Bob"
// Problem: Anyone can access/change name!
name = "Hacker"; // 😱
console.log(getName()); // "Hacker"Problem: name is exposed globally. No privacy!
Step 2: Using Closure (Private Variable)
function createNameStorage() {
let name = "Alice"; // Private! Only accessible inside
return {
getName: function() {
return name; // Closure! Remembers 'name'
},
setName: function(newName) {
name = newName; // Closure! Can modify 'name'
}
};
}
const storage = createNameStorage();
console.log(storage.getName()); // "Alice"
storage.setName("Bob");
console.log(storage.getName()); // "Bob"
// Can't access name directly!
console.log(storage.name); // undefined ✅
console.log(name); // Error: name is not defined ✅What just happened?
1️⃣ Created private variable:
let name = "Alice"; // Only exists inside createNameStorage2️⃣ Created functions that remember it:
getName: function() {
return name; // Closure captures 'name'
}3️⃣ Returned the functions:
return { getName, setName }; // Functions carry 'name' in their closure4️⃣ createNameStorage() finished, but...
- The
namevariable is not deleted - Because
getNameandsetNamestill reference it - It's stored in their closure!
Step 3: Multiple Independent Closures
const storage1 = createNameStorage();
const storage2 = createNameStorage();
storage1.setName("Alice");
storage2.setName("Bob");
console.log(storage1.getName()); // "Alice"
console.log(storage2.getName()); // "Bob"
storage1.setName("Charlie");
console.log(storage1.getName()); // "Charlie"
console.log(storage2.getName()); // "Bob" ← Unchanged!Each closure is independent!
storage1
├─ Closure:
│ └─ name = "Charlie"
└─ Functions: getName, setName
storage2
├─ Closure:
│ └─ name = "Bob"
└─ Functions: getName, setNameThey each have their own private name in their own closure! ✨
The Three Rules of Closures
Rule 1: Functions Remember Their Birthplace
A function remembers where it was created, not where it's called.
function outer() {
const message = "Hello from outer!";
function inner() {
console.log(message); // Remembers outer's 'message'
}
return inner;
}
const myFunc = outer();
// outer() is done, but inner remembers!
myFunc(); // "Hello from outer!" ✨Think of it like this:
inner was born in outer's house
↓
inner remembers everything in outer's house
↓
Even after moving out, inner still knows what was in the house!Rule 2: Closures Capture Variables, Not Values
The closure holds a reference to the variable, not a copy of the value.
function createCounter() {
let count = 0;
return {
increment: function() {
count++; // Modifies the SAME count
},
get: function() {
return count; // Reads the SAME count
}
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.get()); // 2 ← Both functions share count!Visual:
Closure:
┌──────────────┐
│ count = 0 │ ← Same variable!
└──────┬───────┘
│
┌───┴───┐
↓ ↓
increment get
Both point to the SAME countThis is powerful! It means multiple functions can share and modify the same enclosed variable.
Rule 3: Each Call Creates a New Closure
Every time you call the outer function, you get a new, independent closure.
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter1 = createCounter(); // Closure 1
const counter2 = createCounter(); // Closure 2
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 ← Separate closure!
console.log(counter2()); // 2
console.log(counter1()); // 3 ← Still independent!Visual:
Call createCounter() #1
↓
Closure 1: { count: 0 }
↓
counter1 points to Closure 1
Call createCounter() #2
↓
Closure 2: { count: 0 } ← New closure!
↓
counter2 points to Closure 2Why This Is Magic for Reactivity
The Problem: Capturing Effect Context
In reactive systems, when you create an effect, it needs to remember which function to run later.
// We want this to work:
effect(() => {
console.log(state.count);
});
// Later, when state.count changes,
// the system needs to run that EXACT function again!
// How does it remember the function? 🤔Without Closures (Doesn't Work) ❌
let currentEffectFunction = null;
function effect(fn) {
currentEffectFunction = fn; // Just store it
fn(); // Run it once
}
// First effect
effect(() => {
console.log("Effect 1:", state.count);
});
// Second effect
effect(() => {
console.log("Effect 2:", state.name);
});
// Problem: currentEffectFunction is overwritten!
// Only the last effect is remembered! 😱
state.count = 5;
// How do we run the first effect? It's gone!With Closures (Works Perfectly) ✅
function effect(fn) {
const execute = () => {
// This function CAPTURES 'fn' in its closure!
const prevEffect = currentEffect;
currentEffect = execute;
try {
fn(); // Runs the captured function
} finally {
currentEffect = prevEffect;
}
};
execute(); // Run initially
return execute; // Return the closure
}
// Each effect gets its own closure!
const cleanup1 = effect(() => {
console.log("Effect 1:", state.count);
});
const cleanup2 = effect(() => {
console.log("Effect 2:", state.name);
});
// Later, you can run each one independently!
cleanup1(); // Runs effect 1
cleanup2(); // Runs effect 2What's happening:
effect(() => console.log("Effect 1"))
↓
Creates execute function
↓
execute CAPTURES fn in closure
↓
Closure: {
fn = () => console.log("Effect 1")
}
↓
Returns execute (with closure)
↓
cleanup1 holds execute (and its closure)Each effect has its own closure!
cleanup1 (function)
└─ Closure:
└─ fn = () => console.log("Effect 1")
cleanup2 (function)
└─ Closure:
└─ fn = () => console.log("Effect 2")Real Usage in DOM Helpers Reactive
function effect(fn) {
const execute = () => {
const prevEffect = currentEffect;
currentEffect = execute; // ← Closure captures 'execute'
try {
fn(); // ← Closure captures 'fn'
} finally {
currentEffect = prevEffect; // ← Closure captures 'prevEffect'
}
};
execute();
return () => { currentEffect = null; }; // Cleanup closure
}
// Usage
const state = state({ count: 0 });
const cleanup = effect(() => {
// This function is captured in a closure
console.log(state.count);
});
// Later...
state.count = 5;
// The system knows which effect to run because
// it was captured in a closure! ✨Why Closures Are Essential
Closures let us:
1️⃣ Remember the effect function
effect(() => console.log(state.count))
↑
This function is captured and remembered2️⃣ Store it in dependency tracking
deps.get('count').add(execute); // Store the closure3️⃣ Run it later
deps.get('count').forEach(fn => fn()); // Run the captured function4️⃣ Keep context alive
// Even if the original code is far away,
// the closure still has access to all its variables!Common Questions
Q: "When is a closure created?"
Answer: Every time a function is created that references variables from outside itself.
// ✅ Closure created
function outer() {
const x = 10;
return function inner() {
console.log(x); // References outer's 'x' → Closure!
};
}
// ❌ No closure (no external variables)
function standalone() {
const x = 10;
console.log(x); // Only uses its own 'x' → No closure
}
// ✅ Closure created
const y = 20;
function usesGlobal() {
console.log(y); // References global 'y' → Closure!
}Simple rule: If a function uses a variable from outside, it creates a closure over that variable.
Q: "Do closures cause memory leaks?"
Answer: They can, if you're not careful.
Problem case:
function createHugeObject() {
const hugeData = new Array(1000000).fill('data'); // 1 million items!
return function() {
// This closure captures 'hugeData'
// Even if we don't use it!
console.log("Hello");
};
}
const fn = createHugeObject();
// hugeData is still in memory, captured in closure! 😱Solution: Only capture what you need
function createHugeObject() {
const hugeData = new Array(1000000).fill('data');
const summary = hugeData.length; // Extract what you need
return function() {
// Closure only captures 'summary', not 'hugeData'
console.log("Size:", summary);
};
}
const fn = createHugeObject();
// hugeData is garbage collected! ✅Q: "Can closures access 'this'?"
Answer: Yes, but arrow functions behave differently!
function outer() {
this.name = "Outer";
// Regular function: 'this' depends on how it's called
const regular = function() {
console.log(this.name);
};
// Arrow function: captures 'this' from outer
const arrow = () => {
console.log(this.name); // Closure over 'this'!
};
return { regular, arrow };
}
const obj = {
name: "Object",
methods: outer.call({ name: "Outer" })
};
obj.methods.regular(); // "Object" (this = obj)
obj.methods.arrow(); // "Outer" (this from closure)Arrow functions capture this in their closure! 🎯
Q: "What's the difference between closure and scope?"
Scope = Where variables are visible while the code is running
Closure = Variables remembered after the scope is gone
function example() {
const x = 10; // x is in example's scope
function inner() {
console.log(x); // x is accessed via closure
}
return inner;
}
const fn = example();
// example's scope is gone!
// But 'x' lives on in the closure!
fn(); // 10 ← Closure magic! ✨Think of it like this:
Scope = The room you're in
Closure = Taking a photo of the room before leavingPractice Examples
Example 1: Private Counter
Create a counter that can't be directly modified.
function createCounter(start = 0) {
let count = start; // Private!
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
get() {
return count;
},
reset() {
count = start;
return count;
}
};
}
const counter = createCounter(10);
console.log(counter.get()); // 10
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.reset()); // 10
// Can't access count directly!
console.log(counter.count); // undefined ✅Each method is a closure that remembers count and start!
Example 2: Function Factory
Create customized functions with built-in values.
function createMultiplier(factor) {
// 'factor' is captured in closure
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const tenTimes = createMultiplier(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(tenTimes(5)); // 50
// Each function has its own 'factor' in closure!Visual:
double (function)
└─ Closure: { factor: 2 }
triple (function)
└─ Closure: { factor: 3 }
tenTimes (function)
└─ Closure: { factor: 10 }Example 3: Event Handler with Context
Keep context alive for event handlers.
function createButton(id, label) {
const button = document.createElement('button');
button.textContent = label;
let clickCount = 0; // Private counter
// Closure captures: id, label, clickCount
button.addEventListener('click', function() {
clickCount++;
console.log(`Button "${label}" (${id}) clicked ${clickCount} times`);
});
return button;
}
const btn1 = createButton('btn-1', 'Click Me');
const btn2 = createButton('btn-2', 'Press Here');
document.body.append(btn1, btn2);
// Click btn1 3 times:
// Button "Click Me" (btn-1) clicked 1 times
// Button "Click Me" (btn-1) clicked 2 times
// Button "Click Me" (btn-1) clicked 3 times
// Click btn2 once:
// Button "Press Here" (btn-2) clicked 1 times
// Each button's event handler has its own closure! ✨Example 4: Build a Simple Effect System
Create a mini reactivity system using closures.
let currentEffect = null;
const dependencies = new Map();
function track(property) {
if (currentEffect) {
if (!dependencies.has(property)) {
dependencies.set(property, new Set());
}
// Store the closure (currentEffect) as a dependency
dependencies.get(property).add(currentEffect);
}
}
function trigger(property) {
if (dependencies.has(property)) {
// Run all closures that depend on this property
dependencies.get(property).forEach(effect => effect());
}
}
function effect(fn) {
// Create a closure that captures 'fn'
const execute = () => {
currentEffect = execute; // Closure captures itself!
fn(); // Run the captured function
currentEffect = null;
};
execute(); // Run immediately
return execute; // Return the closure
}
// Usage
const state = { count: 0, name: 'Alice' };
// Effect 1 - closure captures this function
effect(() => {
track('count');
console.log('Count is:', state.count);
});
// Count is: 0
// Effect 2 - different closure
effect(() => {
track('name');
console.log('Name is:', state.name);
});
// Name is: Alice
// Change count
state.count = 5;
trigger('count');
// Count is: 5 ← The closure ran!
// Change name
state.name = 'Bob';
trigger('name');
// Name is: Bob ← The other closure ran!Each effect is a closure that remembers its function!
Example 5: Debounce with Closure
Create a debounce function that remembers its timer.
function debounce(fn, delay) {
let timeoutId = null; // Captured in closure
return function(...args) {
// Clear previous timer (from closure)
clearTimeout(timeoutId);
// Set new timer (stored in closure)
timeoutId = setTimeout(() => {
fn(...args); // Execute the captured function
}, delay);
};
}
// Create debounced search
const search = debounce((query) => {
console.log('Searching for:', query);
}, 500);
// Type quickly:
search('a'); // Timer started
search('ap'); // Previous timer cleared, new timer started
search('app'); // Previous timer cleared, new timer started
search('appl'); // Previous timer cleared, new timer started
search('apple'); // Previous timer cleared, new timer started
// After 500ms of no typing:
// Searching for: apple
// Each debounced function has its own timeoutId in closure!Summary
What is a Closure?
A closure is when a function remembers variables from where it was created, even after that place is gone.
function outer() {
const x = 10; // Created here
return function inner() {
console.log(x); // Remembered here!
};
}
const fn = outer();
fn(); // 10 ← x is still remembered! ✨The Mental Model
Think of a function as carrying a backpack:
Function
├─ Code (what it does)
└─ Backpack (closure)
└─ Variables it capturedThe Three Rules
1️⃣ Functions remember their birthplace
- Not where they're called
- Where they were created
2️⃣ Closures capture variables, not values
- They hold a reference
- Changes to the variable are reflected
3️⃣ Each call creates a new closure
- Independent closures
- Don't interfere with each other
Why Closures Matter for Reactivity
Closures let effects remember their context:
effect(() => {
console.log(state.count); // This function is captured
});The closure captures:
- ✅ The effect function itself
- ✅ Which state properties it depends on
- ✅ How to run it later
- ✅ How to clean it up
Without closures:
- ❌ Can't remember which function to run
- ❌ Can't track dependencies
- ❌ Can't create independent effects
- ❌ Reactivity wouldn't work!
Real Usage
// DOM Helpers Reactive uses closures everywhere!
function effect(fn) {
const execute = () => { // ← Closure!
currentEffect = execute; // Captures itself
fn(); // Captures the effect function
currentEffect = prevEffect; // Captures previous
};
execute(); // Run it
return execute; // Return the closure
}Closures are the invisible memory that makes reactive systems remember and work! 🎒✨