# How Closures Work in JavaScript: Complete Guide with Practical Examples

> Master JavaScript closures with our guide. Understand how closures retain scope access and explore practical uses like data encapsulation and function factories. Learn JavaScript concepts effectively.

- Repository: [Leonardo Maldonado/33-js-concepts](https://github.com/leonardomso/33-js-concepts)
- Tags: deep-dive
- Published: 2026-03-03

---

**A closure is a function that retains access to its surrounding lexical scope's variables even after the outer function has finished executing, enabling powerful patterns like data encapsulation, function factories, and memoization.**

This article explores how closures work in JavaScript using practical examples from the [leonardomso/33-js-concepts](https://github.com/leonardomso/33-js-concepts) repository, a comprehensive collection of fundamental JavaScript concepts with test-driven implementations.

## What Is a JavaScript Closure?

A **closure** is created when a function is defined inside another function and references variables from the outer function's scope. When the inner function is returned or passed elsewhere, it maintains a hidden reference to the **lexical environment**—the collection of variables that were in scope at the time of its creation.

Unlike the **execution context** (which is discarded when a function returns), the lexical environment remains alive as long as the inner function exists. This allows the closure to read and modify captured variables indefinitely.

## How Closures Work Under the Hood

### Lexical Scoping vs. Dynamic Scoping

JavaScript uses **lexical scoping**, meaning variable resolution happens based on where the code is written, not where it is called. When a function is created at lines 75-80 in [`tests/fundamentals/scope-and-closures/scope-and-closures.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/fundamentals/scope-and-closures/scope-and-closures.test.js), JavaScript captures the entire variable environment at that moment.

### The Lifetime of Variables

Variables defined in the outer function survive because the inner function maintains a reference to their containing environment. As implemented in `leonardomso/33-js-concepts`, this mechanism enables private state that persists across multiple invocations without polluting the global scope.

## Practical Use Cases Demonstrated in 33-js-concepts

The repository's test suite at [`tests/fundamentals/scope-and-closures/scope-and-closures.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/fundamentals/scope-and-closures/scope-and-closures.test.js) demonstrates several production-ready closure patterns:

### Basic Closure with createGreeter

The simplest form of closure captures a single value from the outer scope. In lines 75-80, the `createGreeter` function demonstrates how a returned function retains access to the `greeting` parameter:

```javascript
function createGreeter(greeting) {
  // `greeting` is captured in the returned function's lexical environment
  return function (name) {
    return `${greeting}, ${name}!`;
  };
}

const sayHello = createGreeter('Hello');
console.log(sayHello('Alice')); // → "Hello, Alice!"

```

### Data Privacy and Encapsulation

Closures enable true private variables by hiding state within the lexical environment. The `createCounter` implementation at lines 92-100 keeps the `count` variable inaccessible from outside while exposing controlled methods:

```javascript
function createCounter() {
  let count = 0;               // private variable
  return {
    inc() { return ++count; },
    dec() { return --count; },
    value() { return count; }
  };
}

const ctr = createCounter();
ctr.inc();   // 1
ctr.inc();   // 2
ctr.dec();   // 1
console.log(ctr.value()); // 1
// `count` is not reachable outside the closure

```

### Function Factories with createMultiplier

At lines 121-130, the repository demonstrates **function factories**—functions that generate specialized functions on demand. Each invocation creates a new closure with its own captured multiplier value:

```javascript
function createMultiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

```

### State Preservation in Event Handlers

The `setupClickCounter` pattern at lines 154-160 shows how closures maintain state across asynchronous events. The handler increments an internal `clicks` variable that survives between event invocations without global namespace pollution.

### Memoization for Performance Optimization

Lines 172-182 implement `createMemoizedFunction`, using a closure-held `cache` object to store expensive computation results:

```javascript
function memoize(fn) {
  const cache = new Map();     // lives in the closure
  return function (arg) {
    if (cache.has(arg)) return cache.get(arg);
    const result = fn(arg);
    cache.set(arg, result);
    return result;
  };
}

const slowSquare = n => { /* heavy work */ return n * n; };
const fastSquare = memoize(slowSquare);

fastSquare(5); // computes
fastSquare(5); // returns cached value

```

## Avoiding the Classic Closure Trap with Var

### The Problem: Shared Bindings in Loops

Lines 108-115 of the test file demonstrate a notorious JavaScript pitfall: when using `var` inside a loop, all created functions share the same variable binding. The final value persists across all closures:

```javascript
const callbacks = [];

for (var i = 0; i < 3; i++) {
  callbacks.push(function() { return i; });
}

console.log(callbacks[0]()); // 3 (not 0)
console.log(callbacks[1]()); // 3 (not 1)

```

### Solutions: let, IIFE, and forEach

Lines 124-138 provide three solutions to create fresh bindings per iteration:

**Using `let` for block scoping:**

```javascript
const callbacks = [];

for (let i = 0; i < 3; i++) {
  callbacks.push(() => i); // each arrow function closes over its own `i`
}

console.log(callbacks[0]()); // 0
console.log(callbacks[1]()); // 1
console.log(callbacks[2]()); // 2

```

**Using an Immediately Invoked Function Expression (IIFE):**

```javascript
for (var i = 0; i < 3; i++) {
  (function(capturedI) {
    callbacks.push(function() { return capturedI; });
  })(i);
}

```

## Summary

- **Closures capture lexical scope**: A function retains access to its birthplace's variables through a hidden reference to the lexical environment, not the execution context.
- **Source implementation**: The `leonardomso/33-js-concepts` repository at [`tests/fundamentals/scope-and-closures/scope-and-closures.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/fundamentals/scope-and-closures/scope-and-closures.test.js) demonstrates closures from basic variable retention (lines 75-80) to advanced memoization patterns (lines 172-182).
- **Privacy without classes**: Closures provide true encapsulation, hiding state from external access while exposing controlled interfaces via returned objects.
- **Factory functions**: Use closures to generate specialized functions like `createMultiplier` or `createTaxCalculator` that remember their configuration.
- **Avoid var in loops**: Use `let`, IIFEs, or `forEach` to prevent all iterations from sharing the same closure binding.

## Frequently Asked Questions

### What is the difference between lexical scope and closure?

**Lexical scope** is the static structure of variable accessibility determined by where code is written in the source file. **Closure** is the runtime mechanism that preserves that scope's variables when a function executes outside its original context. While lexical scope defines what variables are available, closure ensures those variables remain available after the outer function returns.

### How do closures enable data privacy in JavaScript?

Closures create **private variables** by placing them in the outer function's scope and returning only specific methods that can access them. As shown in `createCounter` (lines 92-100), the `count` variable exists in the lexical environment but cannot be accessed directly from outside, effectively simulating private class fields in pre-ES2015 JavaScript or in module patterns.

### Why do loops with `var` create closure bugs and how do you fix them?

`var` is function-scoped, meaning all iterations of a loop share the same variable reference. When closures are created inside the loop, they all capture that single shared reference, resulting in each closure seeing the final value. Fix this by using **`let`** (block-scoped, creating a new binding per iteration), wrapping the loop body in an **IIFE** to create a new scope, or using **`forEach`** instead of a `for` loop.

### When should you avoid using closures?

Avoid closures when **memory leaks** are a concern—captured variables remain in memory as long as the closure exists, which can prevent garbage collection of large datasets. They are also unnecessary for simple, one-off operations where passing explicit parameters is clearer. Additionally, avoid overusing closures in tight loops where the overhead of scope chain lookup might impact performance, though modern JavaScript engines optimize this well.