Skip to content

Function Closures

A closure is a powerful JavaScript feature where an inner function has access to the variables and parameters of its outer (enclosing) function, even after the outer function has finished executing. This concept is fundamental to many advanced JavaScript patterns.

Understanding Scope: Global, Function, and Block Scope

Section titled “Understanding Scope: Global, Function, and Block Scope”

Before diving into closures, let’s recap scope:

  • Global Scope: Variables declared outside any function are global and accessible from anywhere in your code. In browsers, the global object is window.
  • Function Scope: Variables declared with var inside a function are local to that function and cannot be accessed from outside.
  • Block Scope: Variables declared with let and const inside a block (e.g., within {...} of an if statement or for loop) are local to that block.

Example of scope:

const globalVar = "I am global";
function outerFunction() {
const outerVar = "I am in outerFunction";
var functionScopedVar = "I am var in outerFunction"; // Function-scoped
console.log(globalVar); // Accessible
if (true) {
let blockScopedVar = "I am in a block"; // Block-scoped
console.log(blockScopedVar); // Accessible here
console.log(outerVar); // Accessible here
}
// console.log(blockScopedVar); // Error: blockScopedVar is not defined here
function innerFunction() {
const innerVar = "I am in innerFunction";
console.log(globalVar); // Accessible (from global scope)
console.log(outerVar); // Accessible (from outerFunction's scope - Lexical Scoping!)
console.log(functionScopedVar); // Accessible (from outerFunction's scope)
// console.log(blockScopedVar); // Error: blockScopedVar is not defined here (if declared in a separate block)
}
innerFunction();
}
outerFunction();
// console.log(outerVar); // Error: outerVar is not defined here

Lexical Scoping (Static Scoping): JavaScript uses lexical scoping, meaning the scope of a variable is determined by its position in the source code at the time of writing, not by where the function is called. An inner function can ‘see’ variables from its containing (outer) functions.

A closure is formed when an inner function is defined within an outer function, and the inner function is then made available outside the outer function (e.g., by being returned or passed as an argument). The inner function ‘remembers’ its lexical environment (the scope chain) at the time it was created. This means it retains access to the variables of its outer function, even after the outer function has completed execution.

A simple closure example:

function createGreeter(greeting) {
// 'greeting' is part of the outer function's scope
function greet(name) {
// 'greet' is the inner function, forming a closure
// It has access to 'greeting' from its lexical environment
console.log(greeting + ", " + name + "!");
}
return greet; // Return the inner function
}
// Call createGreeter, which returns the 'greet' function
const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");
// Even though createGreeter has finished executing,
// sayHello and sayHi still have access to their respective 'greeting' variables.
sayHello("Alice"); // Output: Hello, Alice!
sayHi("Bob"); // Output: Hi, Bob!

In this example, sayHello and sayHi are closures. Each one ‘closes over’ its own greeting variable from its respective call to createGreeter.

A common problem is creating a counter where the count variable is protected from outside modification but can be incremented by a specific function.

Using a global variable is risky:

let globalCounter = 0; // Exposed, can be changed by any part of the code
function incrementGlobal() {
globalCounter++;
console.log(globalCounter);
}
incrementGlobal(); // 1
globalCounter = 100; // Oops, counter state is compromised
incrementGlobal(); // 101

Using a closure to create a private counter:

function createCounter() {
let count = 0; // 'count' is private to the closure
return function() {
count++;
console.log(count);
return count;
};
}
const counter1 = createCounter();
const counter2 = createCounter();
counter1(); // Output: 1
counter1(); // Output: 2
counter2(); // Output: 1 (counter2 has its own 'count' variable)
// console.log(count); // Error: 'count' is not accessible here

This pattern is often achieved using an Immediately Invoked Function Expression (IIFE) to create the private scope immediately:

const uniqueIdGenerator = (function() {
let id = 0; // Private id variable
return function() {
id++;
return `id_${id}`;
};
})();
console.log(uniqueIdGenerator()); // Output: id_1
console.log(uniqueIdGenerator()); // Output: id_2

In createCounter, the count variable is part of createCounter’s scope. The returned anonymous function is a closure. Each time createCounter is called, a new scope is created, and thus a new count variable is initialized. The returned function maintains a reference to its specific count.

The IIFE version (uniqueIdGenerator) runs once, sets up the private id, and returns the incrementing function. id is protected and can only be modified through the returned function.

  • Data Privacy / Encapsulation: Creating private variables and methods, similar to object-oriented programming (as seen in the counter example).
  • Function Factories: Functions that generate other functions with specific configurations (as seen in the createGreeter example).
  • Event Handlers and Callbacks: Allowing event handlers or callback functions to access variables from the scope in which they were defined, even when executed later. For example, in a loop creating event listeners:
  • Partial Application / Currying: Creating functions that take fewer arguments than the original by fixing some arguments within the closure.
  • Module Pattern: Before ES6 modules, closures (often with IIFEs) were a common way to create modules with private and public members.

Example: Closures in Event Handlers (Classic Problem & Solution)

// Problematic loop without proper closure for 'i'
// (This example might behave as expected with 'let' due to block scoping,
// but demonstrates a classic 'var' issue solved by closures or 'let'.)
// <button>Button 0</button> <button>Button 1</button> <button>Button 2</button>
// Assuming these buttons exist in HTML with ids like 'btn-0', 'btn-1', 'btn-2'
/*
for (var i = 0; i < 3; i++) {
document.getElementById('btn-' + i).addEventListener('click', function() {
// By the time a button is clicked, the loop has finished and 'i' is 3.
console.log('You clicked button: ' + i); // Will always log 3
});
}
*/
// Solution with closure (using an IIFE or another function):
/*
for (var i = 0; i < 3; i++) {
(function(currentIndex) {
document.getElementById('btn-' + currentIndex).addEventListener('click', function() {
console.log('You clicked button: ' + currentIndex);
});
})(i); // Pass 'i' to capture its current value
}
*/
// Modern solution with 'let' (creates a new binding for 'i' in each iteration)
/*
for (let i = 0; i < 3; i++) {
document.getElementById('btn-' + i).addEventListener('click', function() {
console.log('You clicked button: ' + i);
});
}
*/

While let in for loops handles this specific loop scenario elegantly, understanding closures is vital for many other situations where you need to preserve state or context.

  • Closures allow functions to access variables from an outer (enclosing) scope, even after the outer function has executed.
  • They are created every time a function is created, at function creation time.
  • They are a fundamental concept for enabling patterns like data privacy, function factories, and maintaining state in asynchronous operations.
  • Be mindful of memory usage: if a closure holds references to large objects that are no longer needed, it can prevent those objects from being garbage collected. This is less of an issue with modern JavaScript engines but good to be aware of.