Skip to content

Sponsor

Sponsor

Closures (The Memory Keeper)

The Mystery of Remembered Values

Look at this code - there's something magical happening:

javascript
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(); // 2

The Mystery:

  • createCounter() finished running
  • The count variable 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!

javascript
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

javascript
let globalCount = 0;

function increment() {
  globalCount++;
  console.log(globalCount);
}

increment(); // 1
increment(); // 2
increment(); // 3

What's happening:

Global Memory:
┌─────────────────────┐
│ globalCount = 0     │ ← Everyone can access this
└─────────────────────┘

   increment()

   Uses globalCount

Problems:

  • ❌ Anyone can change globalCount
  • ❌ No way to have multiple independent counters
  • ❌ Global namespace pollution
javascript
globalCount = 999; // Oops! Anyone can mess this up!
increment(); // 1000 ← Our counter is broken! 😱

How Closures Work

Function With Closure

javascript
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 it

Visual 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)

javascript
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)

javascript
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:

javascript
let name = "Alice"; // Only exists inside createNameStorage

2️⃣ Created functions that remember it:

javascript
getName: function() {
  return name; // Closure captures 'name'
}

3️⃣ Returned the functions:

javascript
return { getName, setName }; // Functions carry 'name' in their closure

4️⃣ createNameStorage() finished, but...

  • The name variable is not deleted
  • Because getName and setName still reference it
  • It's stored in their closure!

Step 3: Multiple Independent Closures

javascript
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, setName

They 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.

javascript
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.

javascript
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 count

This 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.

javascript
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 2

Why 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.

javascript
// 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)

javascript
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)

javascript
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 2

What'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

javascript
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

javascript
effect(() => console.log(state.count))

This function is captured and remembered

2️⃣ Store it in dependency tracking

javascript
deps.get('count').add(execute); // Store the closure

3️⃣ Run it later

javascript
deps.get('count').forEach(fn => fn()); // Run the captured function

4️⃣ Keep context alive

javascript
// 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.

javascript
// ✅ 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:

javascript
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

javascript
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!

javascript
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

javascript
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 leaving

Practice Examples

Example 1: Private Counter

Create a counter that can't be directly modified.

javascript
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.

javascript
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.

javascript
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.

javascript
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.

javascript
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.

javascript
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 captured

The 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:

javascript
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

javascript
// 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! 🎒✨