JavaScript Closures

Understanding lexical scope and closure patterns in JavaScript

By Hank Kim

JavaScript Closures

Understanding lexical scope and closure patterns in JavaScript

JavaScript Closures

In this document, I want to explain what a closure is, how it works in JavaScript, and why it is important in real-world applications such as React state management and module patterns.

Encapsulation via closure keeps internal state private Figure: Closure enables private state and controlled access.

1. Lexical Scope Basics

JavaScript follows lexical scoping:

  • Variables are only accessible in the scope where they are declared.
  • When code execution leaves that scope, the variables normally become inaccessible.
function foo() {
  let secret = 42;
  console.log(secret); // 42
}
foo();

console.log(secret); // ❌ ReferenceError

πŸ‘‰ secret exists only inside foo. Outside access is impossible.


2. Variable Lifetime in Functions

  • When a function executes, variables inside it are allocated in memory.
  • Once the function returns, those variables are marked for garbage collection since they are no longer needed.
  • This ensures memory efficiency, but prevents external access.

3. The Problem

Sometimes, we want a function to have private state that persists across calls.

Naive solution: use global variables.

  • ❌ Globals remain in memory for the entire program lifetime.
  • ❌ Easy to overwrite accidentally.
  • ❌ Breaks encapsulation.
let count = 0; // global
function increment() {
  count++;
  return count;
}

This works, but exposes count to the entire application.


4. Closures: Functions Remember Their Scope

The solution is a closure. A closure is created when a function returns another function that references variables from its outer scope.

function createCounter() {
  let count = 0; // hidden inside closure

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

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

Simple closure implementation with counter

How it works

  • createCounter finishes execution, but the inner function still references count.
  • The JavaScript engine keeps count alive in memory because it is captured by the closure.
  • Each call to counter() continues from the previous state.

πŸ‘‰ Closure = β€œA function remembers the scope in which it was created.”


5. Independent States

Each call to the closure-creating function generates a new lexical environment.

const counterA = createCounter();
const counterB = createCounter();

console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1 (separate state)

πŸ‘‰ This allows independent states, similar to how multiple React components each maintain their own state.

Two independent counters created from the same factory function Figure: Each closure instance maintains its own lexical environment.


6. React Example

React’s useState hook relies on closure-like behavior.

function App() {
  const [count, setCount] = useState(0);
  // Internally: useState keeps "count" alive across re-renders
}
  • React preserves state values using closures bound to component functions.
  • This is why values persist between re-renders, even though functions re-execute.

useState implemented with closure-like behavior Figure: Simplified view of how React stores state via closures across renders.


7. Encapsulation with Module Pattern

Closures can be used to hide internal state and expose only controlled accessors.

function createUser(name) {
  let _name = name; // private

  return {
    getName: () => _name,
    setName: (newName) => {
      _name = newName;
    },
  };
}

const user = createUser("Alice");
console.log(user.getName()); // Alice
user.setName("Bob");
console.log(user.getName()); // Bob
console.log(user._name); // undefined (cannot access directly)

πŸ‘‰ This pattern achieves data encapsulation, similar to private fields in other languages.


Tags: JavaScript