Hey there! 👋
Closures in JavaScript can be a tricky subject to understand at first. It's also common to be asked about closures in interviews, which is why this post aims to explain how closures work in JavaScript.
What is a closure?
According to mozilla.org, A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.
Let's take a look at an example of a basic closure:
let global = 10;
function print() {
const local = 5;
console.log(local + global);
}
print(); // prints 15
In this example, the print()
function has access to the global
and local
variables in its surrounding scope. This is what we call lexical scoping.
Now, let's see what happens if we change the value of global
after the function declaration:
let global = 10;
function print() {
const local = 5;
console.log(local + global);
}
global = 5; // new value
print(); // prints 10
Here, the print()
function still has access to the global
, even though it was changed after the function was created. This is the power of closures in JavaScript.
In simple terms, closure refers to the ability of an inner function to access variables in the scope of an outer function, even after the outer function has finished executing. This is achieved by the inner function "remembering" the variables from the scope where it was created. Closures are created at the time a function is defined, not when it is called.
This seems pretty simple so far, now let's look at a more concrete example:
function example() {
const num = 5;
// here we are creating a closure
function printNum() {
console.log(num);
}
return printNum;
}
const closureFunction = example();
closureFunction(); // prints 5
In this example, example()
function defines a variable num
and an inner function printNum
that has access to that variable num
from the example() function scope even when the example() function
has finished executing. We return printNum()
function and store it in a variable closureFunction
, so we can call it later and it will still have access to the num
variable and print its value.
In other programming languages, the variable num
would no longer be accessible and may be eligible for garbage collection after the example()
function
has finished executing. However, in JavaScript, the inner function printNum
creates a closure, which "remembers" the variable num from the outer function's
scope and allows it to be accessed even after the outer function has finished executing. This means that the variable num
will not be eligible for garbage
collection until the closure (i.e. the inner function printNum
) is no longer being used.
What can we do with Closures?
Private Methods
Many useful things can be done with closures, one of the most popular is creating private methods.
Let's take a look at the following example:
function createCounter() {
let count = 0;
return {
increment: function () {
count++;
},
getCount: function () {
return count;
},
};
}
const counter = createCounter();
console.log(counter.getCount()); // 0
counter.increment();
console.log(counter.getCount()); // 1
In this example, we have a createCounter
function that returns an object containing two methods, increment
and getCount
.
The increment
method increments the value of a private variable count
, which is only accessible within the scope of the createCounter
function.
The getCount
method returns the current value of count
. By returning an object containing these methods rather than the value of count
,
we have created a private variable that can only be accessed or modified through the provided methods, this is a common use case of closures.
We can also create multiple instances of createCounter
function.
function createCounter() {
let count = 0;
return {
increment: function () {
count++;
},
getCount: function () {
return count;
},
};
}
const counter1 = createCounter();
console.log(counter1.getCount()); // 0
counter1.increment();
console.log(counter1.getCount()); // 1
const counter2 = createCounter();
console.log(counter2.getCount()); // 0
counter2.increment();
console.log(counter2.getCount()); // 1
This allows for better encapsulation and data hiding, making the code more maintainable and scalable.
Function Factories
Another use of closures is to create function factories. A function factory is a function that returns a new function with a specific behavior. The returned function has access to variables in the scope of the function factory, which allows the factory to customize the behavior of the returned function.
For example, let's say we want to create a function that can multiply a number by a specific factor. We can use a closure to create a function factory that takes a factor as an argument and returns a new function that multiplies its input by that factor:
function createMultiplier(factor) {
return function (num) {
return num * factor;
};
}
const doubler = createMultiplier(2);
console.log(doubler(5)); // 10
const tripler = createMultiplier(3);
console.log(tripler(5)); // 15
In this example, we have created two instances of doubler
and tripler
using the createMultiplier
.
Each instance has its own copy of the factor
variable and its own behavior to multiply its input by that factor.
Bonus 🎁
This last section of this post is a way to test your understanding of closures. It is also very common to see this example in technical interviews, so let's take a closer look.
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 500);
}
If we run this code, we will get 1, 2, and 3 printed in the console. Each iteration creates a closure with a new version of the variable let, but what happens if we change let to var?
Let's see an example:
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 500);
}
If we run this code, we will get "3, 3, 3" printed in the console. The reason is that var
is function scoped (i.e. global scoped),
so the variable is shared among all the closures, and by the time the callbacks are invoked, the variable has a value of 3.
This is an important difference between let
and var
we are reusing the same variable so the value is updated by the time the callback is invoked but
with let
we are creating a copy on each iteration with the updated value.
Conclusion
Closures are a very interesting topic in JavaScript, and is fundamental to have a good understanding of how they work.
I hope that by now when you get asked a tricky question in an interview you respond elegantly and knowledgeably.
If you like this post please consider following me: