Closures in JavaScript: The Key to Lexical Scope

Hero image for Closures in JavaScript: The Key to Lexical Scope. Image by Amogh Manjunath.
Hero image for 'Closures in JavaScript: The Key to Lexical Scope.' Image by Amogh Manjunath.

Closures are one of the most fascinating and powerful features of JavaScript. They allow functions to access variables from their parent scope, even after that parent function has been executed (and completed). This is a fairly unique behaviour in programming and stems from JavaScript's lexical scoping. This makes closures an essential tool in every developer's toolkit.

In this article, I intend to explore what closures are, how they work, and their practical applications. By the end, you should hopefully understand why closures are so powerful and how to use them effectively in your projects.


What are Closures?

Basically, a closure is a function that 'remembers' its outer scope even when executed outside of that scope. When a function is declared, it captures the variables from its surrounding environment. This captured environment is preserved even if the outer function has already finished execution.

Here's a simple example:

function createCounter() {  let count = 0;  return function () {    count += 1;    return count;  };}const counter = createCounter();console.log(counter());  // 1console.log(counter());  // 2console.log(counter());  // 3

In this example, the inner function has access to the count variable, even though the createCounter function has already returned. This is a closure in action.


How Do Closures Work?

To understand closures, it's important that we first grasp JavaScript's lexical scoping. When a function is created, it carries along a reference to its outer scope, forming a "chain" of scopes called the scope chain. This mechanism allows the inner function to access variables declared in its parent scope.

The Scope Chain in Action

Here's an example that hopefully illustrates what I'm trying to describe:

function outer() {  let outerVar = 'I am from the outer scope';  function inner() {    console.log(outerVar);  }  return inner;}const innerFunc = outer();innerFunc();  // Logs: "I am from the outer scope"

Here, the inner function retains access to outerVar because of the scope chain established when the inner function was created.


Practical Applications of Closures

Closures aren't just an interesting quirk of JavaScript, they are incredibly useful in everyday programming. Let's look at a few practical scenarios I've cobbled together.

1. Encapsulation and Data Privacy

Closures allow us to emulate private variables, which are not directly accessible from outside the function:

function createSecret(secret) {  return function () {    return secret;  };}const getSecret = createSecret('hidden value');console.log(getSecret());  // 'hidden value'

Here, the secret variable remains private, accessible only through the returned function.

2. Maintaining State

Closures are ideal for maintaining state between function calls, making them invaluable for scenarios where you need a private or persistent value that isn't directly accessible from the global scope. Consider the counter example I've used above: each time the returned function is called, it can access and update the count variable, which remains private to the enclosing function.

This approach is particularly useful when creating reusable components, utilities, or libraries. For example, you might implement a debounce function that tracks the timing of user actions or an authentication module that keeps track of user tokens securely. By encapsulating the state within closures, you avoid global variables and reduce the risk of unintended interactions with other parts of your codebase.

Closures are also a key part of frameworks like React, where they are fundamental for managing local state in hooks like useState and useReducer. These hooks internally rely on closures to "remember" their previous state across renders.

3. Function Factories

Closures allow us to create functions with shared behaviours, for example:

function multiplyBy(multiplier) {  return function (num) {    return num * multiplier;  };}const double = multiplyBy(2);const triple = multiplyBy(3);console.log(double(5));  // 10console.log(triple(5));  // 15

4. Event Listeners

Closures are often used in event listeners to capture and retain values from their outer scope:

function setupListeners() {  for (let i = 1; i <= 3; i++) {    document.querySelector(`#button${i}`).addEventListener('click', function () {      console.log(`Button ${i} clicked`);    });  }}setupListeners();

Common Pitfalls and Considerations

Whilst closures are powerful, they can sometimes lead to unintended behaviours, especially when working with loops.

Closures and Loops

A classic pitfall involves closures in loops, where all closures created within the loop share the same variable:

for (var i = 1; i <= 3; i++) {  setTimeout(function () {    console.log(i);  }, 1000);}// Logs: 4, 4, 4

This happens because var creates a single variable shared across iterations. Using let instead solves this:

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

Wrapping up

Closures are fundamental to JavaScript, enabling us to use powerful features like encapsulation, state maintenance, and function factories.

Key Takeaways

  • Closures allow functions to access variables from their parent scope, even after the parent function has executed.
  • They are a direct result of JavaScript's lexical scoping and the scope chain.
  • Practical uses include data privacy, maintaining state, and creating function factories.
  • Be cautious of common pitfalls, such as closures in loops.

Closures are a cornerstone of JavaScript, empowering developers to write dynamic, efficient code. Mastering them will unlock a deeper understanding of how JavaScript truly works.


Categories:

  1. Development
  2. Front‑End Development
  3. Guides
  4. JavaScript