Closures in JavaScript: Understand Them Thoroughly to Code Like a Pro

Closures—also known as "closed functions"—are one of the most powerful yet confusing concepts in JavaScript. Mastering closures not only helps you ace Senior-level interviews, but also changes how you architect your entire codebase: cleaner, safer, and less buggy. Many people confuse Scope and Closure, but they’re closely related and complementary. Let’s start from the foundations.

closureJavaScriptbiếnphạm viscopehàmfunction
Cover image: Closures in JavaScript: Understand Them Thoroughly to Code Like a Pro
Avatar of Trung Vũ Hoàng

Trung Vũ Hoàng

Author

29/3/20265 min read

1. What Is a Closure?

At its core, a closure is the combination of a function and the lexical environment where that function was defined. In JavaScript, every function is a closure because it remembers the scope where it was created—even after the outer function has finished executing.

Picture this: you have a special box. When you take an item out and close the lid, the item still “remembers” the environment inside the box—as if it carries a piece of the box with it. Closures work exactly like that.

The foundation for this is Lexical Scoping: JavaScript determines a variable’s scope based on where it appears in the source code at write time, not at runtime.

2. How It Works Under the Hood

When a function is called, JavaScript creates an Execution Context to store local variables. Normally, when the function finishes, this context is cleaned up by the Garbage Collector.

But if there’s an inner function that still references variables from the outer function, JavaScript keeps that environment in memory. The inner function “closes over” the variables around it, preventing them from disappearing. That’s why it’s called a closure.

Warning: Overusing closures without understanding memory cleanup can cause memory leaks—subtle bugs where your app consumes more and more RAM without obvious errors.

3. Examples from Basic to Advanced

The easiest way to “see” a closure is with a simple counter:

function createCounter() {
  let count = 0; // This variable belongs to the parent function's scope

  return function() {
    count++;       // The inner function accesses and mutates the parent's variable
    return count;
  };
}

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

The count variable can’t be accessed directly from the outside—it’s “locked” inside the closure. But the returned function can still read and modify it. Each call to counter() continues from the previous value—this is persistent state without needing a class.

One interesting point: if you create counter2 = createCounter(), it will have a completely independent memory space from counter. Each closure owns its own copy of the data.

4. Real-World Use in Professional Code

Closures aren’t academic—they appear everywhere in React, Vue, and common design patterns:

Create Private Variables

Traditional JavaScript doesn’t have a private keyword. Closures are the most natural way to create private variables—hiding internal state while exposing only the necessary methods.

Module Pattern

Wrap your logic in an IIFE and return a public API. This helps avoid polluting the global namespace—crucial in projects with multiple developers.

Currying

Transform a function that takes multiple parameters into a chain of functions that take one parameter each, with each function remembering prior arguments via closures.

Function Factories

Create functions with the same structure but different initial data—such as building event handlers for many buttons at once.

Handling Asynchrony

Capture the correct variable value in loops when working with setTimeout, Promises, or delayed API calls.

5. Pros and Cons

Pros

  • Great data encapsulation—protects internal state.

  • Reduces global variables, preventing logic conflicts.

  • Maintains state across calls without needing classes.

Cons

  • Higher memory usage—variables aren’t freed as soon as the function ends.

  • Nesting many closures can make code hard to debug.

  • May impact performance if you create thousands of closures in tight loops.

Memory optimization tip: When you no longer need a closure, set the variable holding it to null so the Garbage Collector can reclaim the memory.

6. Classic Mistakes to Avoid

A mistake almost every JS beginner makes: using a closure in a for loop with var:

// Wrong result — prints 4, 4, 4 instead of 1, 2, 3
for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

// Correct result — let creates a separate block scope for each iteration
for (let i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i); // Prints 1, 2, 3
  }, 1000);
}

The reason: var is hoisted to function scope—all callbacks share the same i. By the time the timeouts run, the loop has finished and i = 4. The let keyword creates a new block scope per iteration, which fixes this cleanly.

Summary

Closures underpin many critical patterns in modern JavaScript—from React Hooks (even useState relies on closures) to the Module Pattern, Currying, and Memoization. Understanding closures deeply helps you:

  • Write more secure, well-encapsulated code.

  • Avoid subtle bugs related to scope and memory.

  • Read and understand the source code of popular libraries.

Next steps to reinforce your knowledge: explore Hoisting, the Event Loop, and how JavaScript manages memory via the Call Stack and Heap—all closely related to closures.

Found this article helpful?

Contact us for a free consultation about our services

Contact us

Bài viết liên quan