Closures 101

May 26, 2022

Closure forms the basis of many important programming paradigms like modules and functional programming. Functional programming concepts like partial application and currying have closures as their underlying fundamentals.

In this article, we will begin by looking at what closure is, and then further deepen the understanding of it through code snippets of where the concept of closures can be leveraged. We will wrap this up with an interesting mental model that will make the understanding of closures easier to reason about.

At a glance:

Closure? What?

Closure is a bundled combination of a function and references to variables from its outer lexical scope. For closure to be observed, the function needs to be invoked in a scope apart from where it is defined, specifically in a different branch of the scope chain. a different scope where access to those variables wouldn't had been possible.

Let's start with a basic example that demonstrates how closures work:

function findPerson(personId) {
    let persons = [
        { id: 1, name: 'Prerak' },
        { id: 2, name: 'Akshat' },
        { id: 3, name: 'Nitin' },
    ];

    return function greetPerson(greeting) {
        var person = persons.find((person) => person.id === personId);
        console.log(`${greeting} ${person.name}`);
    };
}

var greetPersonWithId1 = findPerson(1);
greetPersonWithId1('Hello'); // Hello Prerak

greetPerson function being returned from the function findPerson gets stored in greetPersonWithId1. But how greetPersonWithId1 function is able to access the persons array and the parameter personId given that it has no access of them lexically? This is where closure comes into play. When greetPerson is returned, it is returned as its closure: the function itself along with the references to its surrounding variables. Alternatively speaking, greetPerson closed over the variable persons and personId - the variables in its parent's scope.

Remember, for closure to be observed, the function should be invoked in different scope branch from where it is defined. Let's look at another example which shows accessing variable from the outer lexical scope:

function greet(name) {
  var greeting = "Hello"
  logGreeting()
  logGreeting() {
    console.log(`${greeting} ${name}`)
  }
}

greet("Prerak") // Hello Prerak

Here the function logGreeting is called in the same scope in which it is defined. logGreeting function is able to access the greeting variable by lookup in its outer scope. The observation here is of lexical scope and not closure.

Closure is a behavior of functions only. Anything except a function, say, an object or a class do not exhibit closure.

Closures have references not values

Let's take a simple function counterFactory that returns another function getCurrentCount which returns incremented counter everytime it is invoked.

function counterFactory() {
    var count = 0;

    return function getCurrentCount() {
        count = count + 1;
        return count;
    };
}

var incrementCounter = counterFactory();

console.log(incrementCounter()); // 1
console.log(incrementCounter()); // 2
console.log(incrementCounter()); // 3

getCurrentCount "closes over" the count variable as a reference to it, rather than a snapshot of a initial value of 0, which results in increase in count everytime incrementCounter is invoked.

Closures In Callbacks And Event Handlers

Callbacks and event handlers / event listeners fundamentally use closures.

function cb(...args) {
  // cb does some work
}

function func(...args, cb) {
  // func does some work
  cb(...args)
  // func does some more work
}

Let's say callback cb executes at some later point of time long after function func has finished its execution. But how does cb has access to the arguments which vanished with the execution of func. It is the closure that persists the arguments being passed in cb's outer scope.

Closures come into play in event handlers too -

function clickListener(btn, id) {
    btn.addEventListener('click', function onClick() {
        console.log(`The button with id ${id} button was clicked!`);
    });
}

let btn = document.getElementById('login-btn');
clickListener(btn, 'login-btn');

id is accessible to the event listener function even after clickListener finished its execution.

Mental Model: Encapsulation

When we need to access variables that are not in the current scope, we move to the next outer lexical scope until we find that variable or the until the global scope is reached.

As a closing note, this mental model comes pretty handy: instead of placing the variables in outer scopes, we encapsulate them in nothing but a closure. A function with access to variables via closure can be used without providing the input again and again, which makes the code cleaner and more performance effecient.