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?
- Closures Have References Not Values
- Closures In Callbacks And Event Handlers
- Mental Model: Encapsulation
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.
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.