Understanding Closures:
Capturing Lexical Environments

Published on: April 1, 2025

No title

In this article, we'll explore the concept of closures in JavaScript. We'll discuss what closures are, how they work, and how they can be used to create powerful and flexible code.

What are closures?

A function declared / defined inside another function has access to the outer function's scope, even after the outer function has returned. This is called a closure.

javascript
function outerFunction() {
	const outerVariable = 'I am from outer function';

	return function () {
		console.log(outerVariable);
	};
}

const innerFunction = outerFunction();
innerFunction(); // I am from outer function

innerFunction() still has access to the outerFunction() scope, even after outerFunction() has returned

How do closures work?

A lexical environment is the environment in which a piece of code is executed. It consists of the variables that are in scope at that time, as well as a reference to the outer lexical environment. When a function is defined, it captures its lexical environment, creating a closure.

mermaid
flowchart TD
	classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
	classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
	classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
	classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
	classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

	subgraph "Global Environment Scope"
		global_variables[Variables declared in Global Scope]
		subgraph "Outer Function Environment Scope"
			outer_variables[Variables declared in Outer Function Scope]
			innerFunction[Inner Function]
		end
	end
	global_variables -.-> |ref| outer_variables -.->|ref,
 retained even after upstream scope exits| innerFunction

This is what enables closures to work in JavaScript.

What makes closures cool?

Data Privacy with Closures

Closures enable use to create private variables in JavaScript. That cannot be directly accessed from outside the function.

javascript
function createCounter() {
	let count = 0;

	return {
		increment: () => {
			count++;
			return count;
		},
		decrement: () => {
			count--;
			return count;
		},
	};
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.count; // undefined

Asynchronous Code

Closures help maintain the execution context of an asynchronous task.
The callback function passed to an asynchronous task has access to the variables in the outer scope, even after the outer function has returned.

javascript
function attachEventHandlers(buttonId) {
	const button = document.getElementById(buttonId);

	button.addEventListener('click', function () {
		// buttonId is still accessible here
		console.log(`Button ${buttonId} clicked`);
	});
}

attachEventHandlers('btn-1');
attachEventHandlers('btn-2');
mermaid
sequenceDiagram
	participant MainThread
	participant EventLoop
	
	Note over MainThread: outerFunction() called

	MainThread->>EventLoop: callback function passed to addEventListener

	Note over MainThread: outerFunction() returns

	EventLoop-->>EventLoop: Executes callback function on event trigger

callBack() still has access to the buttonId variable, even after outerFunction() has returned

Common Pitfalls

Shared Scope Issues

When using closures, be careful of shared scope issues. If multiple functions share the same outer scope, they will share the same variables.

javascript
function createFunctions() {
  const functions = [];

  for (var idx = 0; idx < 3; idx++) {
    functions.push(function () {
      console.log(idx);
    });
  }

  return functions;
}

const funcs = createFunctions();
funcs[0](); // Output: 3 (not 0)
funcs[1](); // Output: 3 (not 1)
funcs[2](); // Output: 3 (not 2)

In this example, all the functions share the same idx value. Because var keyword is function-scoped instead of block-scoped.
In each iteration of the loop, the same idx variable is reassigned a new value.

javascript
for (let idx = 0; idx < 3; idx++) {

Switching from var to let fixes the issue.

You can learn more about the differences between var, let, and const in JavaScript in the following article:

The Building Blocks: Variables and Values in JavaScript

Memory Leaks

When using closures, be mindful of memory leaks. If a closure holds a reference to a large object, it can prevent the object from being garbage collected.

javascript
function createLargeClosure() {
  let largeArray = new Array(1000000).fill(0);

  return function () {
    console.log("Closure created");
  };
}

const largeClosure = createLargeClosure();

largeArray var is retained in memory, until the largeClosure func is no longer needed. (eg: by setting the value of largeClosure to null)

Conclusion

Closures are a powerful feature of JavaScript that enable us to create flexible and maintainable code.

By understanding how closures work and how to use them effectively, you can take your JavaScript skills to the next level. Just be mindful of the common pitfalls and best practices.