Skip to content
C Codeloom
JavaScript

JavaScript Closures and Scope

Understand JavaScript closures and lexical scope — see how inner functions remember outer variables, and learn the patterns that closures enable.

·4 min read · By Codeloom
Beginner 9 min read

What you'll learn

  • What lexical scope means in JavaScript
  • How closures capture variables
  • Practical uses like private state
  • A classic loop variable pitfall
  • Memory considerations with closures

Prerequisites

  • Familiarity with JavaScript basics

Closures are one of the most powerful features in JavaScript and one of the most misunderstood. Once you understand them, patterns like data hiding, currying, and React hooks all start to make sense. This tutorial demystifies closures step by step.

Lexical Scope

JavaScript uses lexical scoping, which means a variable is resolved based on where it is written in the source code, not where it is called from. The location of a function in your file determines which outer variables it can see.

const greeting = 'Hello';

function greet() {
  console.log(greeting); // 'Hello'
}

greet();

Even if you call greet from a completely different module, it still resolves greeting from where it was defined.

What Is a Closure?

A closure is the combination of a function and the lexical environment it was created in. When a function references variables from an outer scope, those variables stay alive as long as the inner function does — even after the outer function has returned.

function makeCounter() {
  let count = 0;
  return function () {
    count += 1;
    return count;
  };
}

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

The inner function “closes over” count. The variable does not get garbage-collected because the returned function still references it.

Private State

Closures give you encapsulation without classes. The outer function defines variables that nothing outside can touch, while the returned functions form a public API.

function createWallet(initial) {
  let balance = initial;
  return {
    deposit(n) { balance += n; },
    withdraw(n) {
      if (n > balance) throw new Error('Insufficient');
      balance -= n;
    },
    getBalance() { return balance; },
  };
}

const w = createWallet(100);
w.deposit(50);
console.log(w.getBalance()); // 150

There is no way to access balance directly from outside. This pattern predates class fields and is still useful in many cases.

Function Factories

Closures let you create families of related functions by capturing parameters.

function multiplyBy(factor) {
  return n => n * factor;
}

const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(5), triple(5)); // 10 15

Each returned function carries its own captured factor. This is the building block of currying and partial application in functional programming.

The Classic Loop Pitfall

Before let was introduced, the most famous closure bug involved var inside a loop. Because var is function-scoped, every iteration shares the same variable.

// Buggy with var
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// Logs 3, 3, 3

Using let fixes the bug because let creates a new binding for each iteration.

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

This single change saved millions of developer-hours when ES6 landed.

Closures in Callbacks

Whenever you write an event handler, a setTimeout callback, or a promise then, you are creating a closure. The callback can see variables from the surrounding function even though it executes later.

function attach(button, label) {
  button.addEventListener('click', () => {
    console.log(`Clicked: ${label}`);
  });
}

Each call to attach produces a handler with its own captured label. This is exactly how React event handlers work too.

Memory Considerations

Because closures keep references alive, holding on to a closure can keep large objects from being garbage-collected. This matters in long-running apps or when capturing big DOM elements.

If you create many closures and store them, make sure they only capture the data they actually need. Destructuring at the top of the outer function is one way to limit what gets captured.

Wrapping Up

Closures are not magic — they are just the natural consequence of lexical scope and first-class functions. Once you see them in action, you will notice them everywhere: in React hooks, in middleware, in iterators. Master them, and a huge swath of JavaScript will suddenly click.