JavaScript Closures with examples
Aditya Tripathi
Posted on September 30, 2023
In JavaScript, Closure is a phenomena of a code getting executed outside its lexical scope. They happen because of writing lexically structured code.
In other words, Closures let us access a foreign parent scope even after it has exited the runtime. Closures are created every-time we create a scope. They are strongly associated with scopes so before exploring Closures, let us refresh what lexical scopes are.
Lexical Scope
You might be already familiar with the concept of Scope and Execution contexts in JavaScript. If you would like a refresher on these topic, consider a recent article I wrote, similar to this: Mastering JavaScript Scopes
Let's cover the important highlights here briefly.
JavaScript associates important execution details of a program with the place they are declared in the source code, pertaining to the visibility of declared entities. At author time, these are called Scopes. At runtime these become Execution Contexts and are called on Execution Stack one after another to hold the information required for successful execution of the code.
Consider the following example:
function greet(name, typeOfGreeting){
let nameInUpper = name.toUpperCase();
function sayHello(){
const greetings = `Hello ${nameInUpper} :)`
return greetings;
}
function sayHi(){
const greetings = `Hi ${nameInUpper}!`
return greetings;
}
return typeOfGreeting === 'hi' ? sayHi : sayHello;
}
In the above example, greet
function creates a functional scope. This scope contains two other functions, sayHello
and sayHi
along with greet
arguments and variable nameInUpper
.
Note that nested scope can access declared entities of the enclosing scope. This means both sayHello
and sayHi
can access entities of greet
(and Global Scope).
However, a parent scope cannot access entities declared in its nested scopes. So greet
doesn't have access to entities declared inside sayHi
and sayHello
.
Also, note that two greetings
variables are created without conflict because they are in separate scopes, i.e. one in sayHi
and one in sayHello
.
Closures in action
Now that we understand scopes, lets explore how closures "happen" as a result of writing lexically scoped code.
When we execute greet
like below:
const greetBen = greet("ben", "hi");
console.log(greetBen()) // Hi BEN!
We notice a rather peculiar behaviour. On executing greetBen
we are able to remember entities declared in greet
and use them at later point of time in code (i.e. in console.log()
), even when greet
has completed its execution well before executing greetBen
.
This is Closures! greetBen
is said to have formed a closure on greet
. This allows greetBen
to keep the entities declared in greet
"alive" (i.e. stop being garbage collected), even after its execution context has been popped out of execution call stack.
For better understanding, consider a slightly modified example below:
const greetBen = greet("ben", "hi");
setTimeOut(() => console.log(greetBen()), 5000)
The above code prints the same output as before but after 5 seconds, re-enforcing our understanding of keeping the entities alive and dynamically accessing a them at runtime even when the scope in which they were declared is not available.
Closure Examples
Now that we have seen some basic examples, let us discuss a couple of advanced ones.
Consider the following code:
(function iife1(a) {
return (function iife2(b) {
console.log(a); // What is logged?
})(1);
})(100);
After a good look, we recognise that iife2
forms a closure with iife1
and hence has access to a
. The output of the program will be 100.
Consider another code-snippet:
for (var i = 0; i < 3; i++) {
setTimeout(function print() {
console.log(i); // What is logged?
}, 1000);
}
Because var
declarations do not create block scopes, the closure formed by print
on the enclosing scope (i.e. Global Scope) will get the final updated value of i
. Hence the above code will print 3
, 3
, 3
.
If we were to replace var
with let
, then the enclosing scope for print
will be for
block instead of the Global Scope, as let
creates a block scope. On each loop iteration, a new scope will be created and the updated i
value will be generated for each new scope, resulting in sustaining each value as the loop iterates. Hence the output would then be 0
, 1
and 2
.
I have borrowed these examples from an amazing blog by Dimitri Palvutin. You can find more of such examples by visiting his write-up here.
Design Patterns
JavaScript developers have been utilising Closures to create some of the most famous design patterns in the language. Some of them are discussed below:
Functions as first class citizens
We are able to pass around functions in JavaScript due to closures. A function as argument forms the closure with the calling function.
function doMath(value, operation){
return operation(value)
}
function addFive(number){
return number + 5;
}
function multiplyFive(number){
return number * 5;
}
console.log(doMath(5, addFive)); // 10
console.log(doMath(5, multiplyFive)); // 25
Here, operation
function is able to form a closure with doMath
enabling us to operate on doMath
argument. It is using this behaviour, we are able to implement callbacks and event-listeners in the language.
Module Pattern
Closures are utilised to create data encapsulation and data abstraction. Consider the example below:
function CounterModule(){
// Data Encapsulation
let counter = 0;
function reset(){
counter = 0
}
function increment(){
counter++;
}
function decrement(){
counter--;
}
function getCounter(){
return counter;
}
return {
reset,
increment,
decrement,
getCounter
}
}
// Data Abstraction
let {reset, increment, decrement, getCounter} = CounterModule();
}
In the example above, we created a CounterModule
with a private implementation detail counter
. We then define operations allowed to be performed on this private data by using functions like reset
, increment
and decrement
because these function form closure with CounterModule
.
Thats all what Closures are!
I hope that this article helped you understand Closures and how it is utilised in everyday JavaScript code to implement some of the most important language behaviours and characteristics.
Until next time. Happy Hacking! :)
Posted on September 30, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.