JavaScript Closures Explained with Real Examples

JavaScript Closures Explained with Real-World Examples

JavaScript closures let a function remember and access variables from its lexical scope even after the outer function has finished running. They power private variables, function factories, memoization, event handlers, and patterns used across modern JavaScript frameworks. A closure basically mixes scope, memory, and execution context into one extremely useful abstraction.


Introduction

Closures are one of those concepts everyone hears about, most people pretend to “kind of know,” and only a few actually understand properly. But once it clicks, your whole JavaScript game levels up — suddenly React hooks make sense, memoization stops sounding fancy, and your event handlers behave exactly how you expect.

To truly grasp closures, you need to understand lexical scoping, how execution contexts work under the hood, and why JavaScript sometimes hangs onto variables longer than you expect.

In this article, we’ll break closures down step-by-step and then walk through practical, real-world examples that you’ll actually use as a developer.


What Is a Closure?

A closure is formed when a function “remembers” variables from the environment where it was created — even after that environment is gone.

  • The function itself
  • The lexical environment (the surrounding scope)
  • Any captured variables the function uses

As long as an inner function exists somewhere in your app, JavaScript keeps its surrounding environment alive.


Lexical Scoping and Execution Contexts

How Lexical Scoping Works

JavaScript uses lexical scope, which simply means where you write your function determines what variables it can access — not where you call it.

Execution Context Lifecycle

Every time a function runs, JS creates a fresh execution context containing:

  • A variable environment
  • A lexical environment linking to outer scopes
  • The this binding

Normally, when a function returns, its context is destroyed. Unless a closure is involved — then JS keeps those variables alive.


Example 1: Basic Closure in Action

function outer() {
 let count = 0; // captured variable

 function inner() {
   count++;
   return count;
 }

 return inner;
}

const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

`count` survives because `inner()` still references it. That’s closures in a nutshell.


Why Closures Persist Memory

JavaScript’s garbage collector cleans up memory only when things are unreachable. Here:

  • `inner` still exists.
  • `inner` uses `count`.
  • JS keeps `count` alive.

Great for functionality, but can cause headaches if you’re not careful with long-lived closures.


Real-World Use Case 1: Private Variables and Encapsulation

Closures were the OG way to create private variables before JavaScript had `#private` fields.

Example: Private Counter Module

function createCounter() {
 let value = 0; // private

 return {
   increment() {
     value++;
     return value;
   },
   getValue() {
     return value;
   }
 };
}

const counter = createCounter();
counter.increment(); // 1
console.log(counter.getValue()); // 1
console.log(counter.value); // undefined

Real-World Use Case 2: Function Factories

Example: Create a Logger with Namespaces

function createLogger(prefix) {
 return function(message) {
   console.log(`[${prefix}] ${message}`);
 };
}

const info = createLogger('INFO');
const debug = createLogger('DEBUG');

info('Server started');
debug('Cache miss occurred');

Real-World Use Case 3: Event Handlers and DOM State

Example: Button Click Counter

function attachClickCounter(button) {
 let clicks = 0;

 button.addEventListener('click', () => {
   clicks++;
   console.log(`Clicked ${clicks} times`);
 });
}

attachClickCounter(document.querySelector('#myButton'));

Real-World Use Case 4: Memoization for Performance

Example: Basic Memoization Wrapper

function memoize(fn) {
 const cache = {};

 return function(key) {
   if (key in cache) {
     return cache[key];
   }

   const result = fn(key);
   cache[key] = result;
   return result;
 };
}

function slowDouble(n) {
 for (let i = 0; i < 1e7; i++);
 return n * 2;
}

const fastDouble = memoize(slowDouble);
console.log(fastDouble(5)); // slow
console.log(fastDouble(5)); // fast

Closures and Loops: A Common Trap

Classic Bug

for (var i = 0; i < 3; i++) {
 setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3

Fix with let

for (let i = 0; i < 3; i++) {
 setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2

Closures in Modern JavaScript

Closures are everywhere today — you probably use them daily without realizing it.

React Hooks

`useState`, `useCallback`, `useMemo` — all closure magic under the hood.

Async/Await

Even when async functions pause, they remember surrounding variables.

Functional Programming

Currying, partial application, higher-order functions — closures power all of it.


Performance Considerations

  • Huge objects captured accidentally
  • Event listeners that never get removed
  • Memoization caches that grow forever

A closure that holds more than it needs is like a suitcase stuffed with junk you never unpack.


When NOT to Use Closures

  • You need clean, stateless functions
  • The captured scope is too large
  • A class or module is simpler to maintain

FAQ

1. Are closures the same as scopes?

No. Scope decides what’s visible. A closure is a function bundled with its captured scope.

2. Do closures impact memory usage?

Yes. Captured variables stay in memory as long as the closure exists.

3. Can closures cause memory leaks?

Definitely, especially with long-lived listeners and unused references.

4. Are closures faster or slower?

Neither. The main cost is memory retention, not CPU.

5. Do all JavaScript functions create closures?

Technically yes, but a closure is only active when an inner function uses variables from an outer scope.

Leave a Comment

Your email address will not be published. Required fields are marked *