JavaScript Closures Explained

JavaScript Closures Explained

Closures in JavaScript are a powerful concept that enables an inner function to access variables from its outer function even after that outer function has finished running. In essence, a closure is the pairing of a function and the lexical environment in which it was defined. This unique relationship allows variables to persist beyond their original scope, creating lasting contexts that live on in memory.

By understanding closures, you can write functions that remember state, create private variables, and build dynamic behaviors that feel almost magical. In this article, we will explore how closures work in various scenarios—returning functions, loops, IIFEs, DOM interactions—and see how they let inner functions keep access to their outer variables even after those outer contexts no longer execute.

Creating a Simple Closure

First, let’s see how a basic closure works. You define an outer function that declares a variable and returns an inner function. That inner function continues to have access to the outer variable long after the outer function has run.

<!DOCTYPE html>
<html>
<head>
  <title>Spellbook Closure</title>
</head>
<body>

<script>

  function createSpellCaster(spell) {

    return function() {
      console.log('Casting the spell:', spell);
    };

  }

  const fireCaster = createSpellCaster('Fireball');
  fireCaster(); // Casting the spell: Fireball

</script>

</body>
</html>

Here, createSpellCaster returns an inner function that logs the spell name. Even though createSpellCaster has finished executing, the inner function still remembers the spell variable. That is closure in action.

Closures in Action: Returning Functions

Closures shine when you return a function that holds state. For example, a potion mixer that retains its ingredients can be built with a closure.

<!DOCTYPE html>
<html>
<head>
  <title>Potion Factory</title>
</head>
<body>

<script>

  function potionMaker(ingredient) {

    return function(quantity) {
      console.log(`Mixing ${quantity} parts of ${ingredient}`);
    };

  }

  const lovePotion = potionMaker('Rose Petal');
  lovePotion(3); // Mixing 3 parts of Rose Petal

</script>

</body>
</html>

The inner function retains ingredient, so calling lovePotion(3) still knows it was made from rose petals. Closures let state travel across calls.

Closures and Parameters

When closures grab parameters from their outer function, you can create dynamic, personalized tools. Consider an animal factory that returns a function with the animal’s sound:

<!DOCTYPE html>
<html>
<head>
  <title>Animal Sound</title>
</head>
<body>

<script>

  function createAnimal(name, sound) {

    return function() {
      console.log(`${name} says "${sound}"`);
    };

  }

  const dog = createAnimal('Dog', 'Woof');
  const cat = createAnimal('Cat', 'Meow');
  dog(); // Dog says "Woof"
  cat(); // Cat says "Meow"

</script>
</body>
</html>

Each returned function keeps its own name and sound, so closures personalize behavior dynamically.

Closures with Event Listeners

Closures also power interactive DOM behaviors. Let’s build multiple buttons that remember their identity:

<!DOCTYPE html>
<html>
<head>
  <title>Button Memory</title>
</head>
<body>

<button id="btn1">Button 1</button>
<button id="btn2">Button 2</button>

<script>

  function makeClickHandler(name) {

    return function() {
      alert(`You clicked ${name}`);
    };

  }

  document.getElementById('btn1').addEventListener('click', makeClickHandler('Button 1'));
  document.getElementById('btn2').addEventListener('click', makeClickHandler('Button 2'));

</script>

</body>
</html>

Each handler remembers its own name, so clicking either button shows the correct alert—thanks to closures preserving that context.

Closures Inside Loops

When closures are created in loops, the use of let or var changes how variables are captured. Using let in the loop correctly isolates each closure:

<!DOCTYPE html>
<html>
<head>
  <title>Treasure Loop</title>
</head>
<body>

<div id="dungeon"></div>

<script>

  for (let i = 1; i <= 3; i++) {

    const btn = document.createElement('button');
    btn.textContent = `Chest ${i}`;

    btn.addEventListener('click', function() {
      alert(`You opened Chest ${i}!`);
    });

    document.getElementById('dungeon').appendChild(btn);

  }

</script>

</body>
</html>

Each button’s click handler remembers its own i value. If var had been used, all handlers would refer to the final value—closure and block scope interplay in action.

Closures for Private Variables

Closures let you simulate private variables. You can build a “pet dragon” object that keeps its treasure hidden:

<!DOCTYPE html>
<html>
<head>
  <title>Private Treasure</title>
</head>
<body>

<script>

  function dragonPet(name) {

    let treasure = 0;

    return {

      addTreasure(amount) {
        treasure += amount;
        console.log(`${name} now has ${treasure} treasures.`);
      },

      showTreasure() {
        console.log(`${name} possesses a total of ${treasure} treasures.`);
      }

    };

  }

  const draco = dragonPet('Draco');

  draco.addTreasure(5); // Draco now has 5 treasures.
  draco.addTreasure(2); // Draco now has 7 treasures.
  draco.addTreasure(4); // Draco now has 11 treasures.

  draco.showTreasure(); // Draco possesses a total of 11 treasures.

</script>

</body>
</html>

Here treasure is private and only accessible via the returned methods. The closure preserves that private state.

IIFEs and Closures

Immediately Invoked Function Expressions (IIFEs) make closures that run instantly and retain hidden variables. For example:

<!DOCTYPE html>
<html>
<head>
  <title>IIFE Magic</title>
</head>
<body>

<script>

  const reveal = (function(secretPhrase) {

    return function() {
      console.log(`The secret phrase is "${secretPhrase}"`);
    };

  })('Open Sesame');

  reveal(); // The secret phrase is "Open Sesame"

</script>

</body>
</html>

The IIFE runs immediately with secretPhrase, and the returned function carries that phrase in its closure forever.

Nested Closures

You can nest closures to hold multiple levels of state. Suppose a wizard academy has levels of spells:

<!DOCTYPE html>
<html>
<head>
  <title>Nested Closures</title>
</head>
<body>

<script>

  function academy(level) {

    return function(student) {

      return function(action) {

        return function() {
          console.log(`Level ${level}, Student ${student} will now ${action}.`);
        };

      }

    };

  }

  const level1 = academy(1);
  const alice = level1('Alice');
  const actionFn = alice('cast a spell');
  actionFn(); // Level 1, Student Alice will now cast a spell.

</script>

</body>
</html>

Each inner function retains its own level, student, and action from earlier closures.

Fun with Closures in Real DOM Interactions

Finally, let’s build a quiz where each question button has its own closure storing the correct answer:

<!DOCTYPE html>
<html>
<head>
  <title>Quiz with Closures</title>
</head>
<body>

<div id="quiz"></div>

<script>

  const questions = [
    { question: 'Spell color of Fireball?', answer: 'Red' },
    { question: 'Elixir of healing color?', answer: 'Green' }
  ];

  questions.forEach((q, i) => {

    const btn = document.createElement('button');
    btn.textContent = q.question;

    btn.addEventListener('click', function() {
      alert(`Correct answer: ${q.answer}`);
    });

    document.getElementById('quiz').appendChild(btn);

  });

</script>

</body>
</html>

Each button’s handler remembers the corresponding answer through the closure—making quiz interactions stateful and self-contained.

Conclusion

Closures let JavaScript functions carry their outer context with them, enabling them to remember variables, parameters, and nested environments long after outer functions have finished. Whether you’re returning functions, using closures in loops, simulating private variables, or building interactive DOM elements, closures unlock expressive, dynamic behavior. Understanding closures equips you to create memory-aware functions and encapsulated features that feel intelligent and fun to work with.

References

If you’re curious to explore further or want to double-check what you’ve learned, these trusted documentation pages offer more detailed explanations and examples:

Scroll to Top