Introduction
Closures are a somewhat abstract concept of the JavaScript language and sneak into the compiler-side of programming. However, understanding how JavaScript interprets functions, nested functions, scopes and lexical environments is imperative to harnessing its full potential.
In this article, we will try to demystify said concepts and provide a simple guide to JavaScript Closures.
What is a Closure?
First let's take a look at the official MDN definition of closure:
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 simpler terms, a closure is a function that has access to an outer function's scope. To understand this, let's take a look at how scopes work in JavaScript.
Scope in JavaScript
Scope determines which variables are visible or can be referenced in a given context. Scope is broadly divided into two types - Global Scope and Local Scope:
-
Global Scope - variables defined outside a function. Variables in this scope can be accessed and altered from anywhere in the program, hence the name "global".
-
Local Scope - variables defined inside a function. These variables are specific to the function in which they are defined, hence named "local".
Let's take a look at a global and local variable in JavaScript:
let name = "Joe";
function hello(){
let message = "Hello";
console.log(message + " " +name);
}
In the example above, scope of name
is global, i.e. it can be accessed anywhere. On the other hand, message
is defined inside a function, its scope is local to the hello()
function.
JavaScript uses Lexical Scoping when it comes to function scopes. Meaning that the scope of a variable is defined by the position of its definition in the source code. This lets us reference global variables within smaller scopes. A local variable can use a global variable, but vice-versa isn't possible.
On
function outer(){
let x = 10;
function inner() {
let y = 20;
console.log(x);
}
inner();
console.log(y)
}
outer();
This code results in:
10
error: Uncaught ReferenceError: y is not defined
The inner()
function can reference x
since it's defined in the outer()
function. However, the console.log(y)
statement in the outer()
function cannot reference the y
variable because it's defined in the inner()
function's scope.
Additionally, in this scenario:
let x = 10;
function func1(){
console.log(x);
}
function func2() {
let x = 20;
func1();
}
func2();
The output will be:
10
When we call func1()
from within func2()
, we've got a locally scoped variable x
. However, this variable is totally irrelevant to func1()
as it's not accessible in func1()
.
Thus, func1()
checks if there's a global variable with that identifier available, and uses it, resulting in the value of 10
.
Closures Under the Hood
A closure is a function which has access to it's parent's variables even after the outer function has returned. In other words, a closure has three scopes:
- Local Scope - Access to variables in its own scope
- Parent Function's scope - Access to variables within its parent
- Global Scope - Access to global variables
Let's take a look at a closure at work, by making a function that returns another function:
function outer() {
let x = 3
return function inner(y) {
return x*y
}
}
let multiplyByThree = outer();
console.log(multiplyByThree(2));
This results in:
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
6
If we do a:
console.log(multiplyByThree);
We're greeted with:
function inner(y) { return x * y; }
Let's go through the code step-by-step to see what is happening under the hood:
- The
outer()
function is defined in global scope. outer()
is invoked, and it returns a function that is assigned tomultiplyByThree
.- New execution context is created for
outer()
.- Variable
x
is set to 3.
- Variable
- Returns a function named
inner()
. - The reference to
inner()
is assigned tomultiplyByThree
. - As the outer function finishes execution, all the variables within its scope are deleted.
- New execution context is created for
- Result of the function call
multiplyByThree(2)
is logged to the console.inner()
is invoked with2
as the argument. So,y
is set to2
.- As
inner()
preserves the scope chain of its parent function, at the time of execution it will still have access to the value ofx
. - It returns
6
which gets logged to the console.
In conclusion, even after the outer function ceases to exist, the inner function has access to the variables defined in the scope of the outer function.
Visualizing Closures
Closures can be visualized through the developer console:
function outer() {
let x = 3
return function inner(y) {
return x*y
}
}
let multiplyByThree = outside();
console.dir(multiplyByThree);
By executing the code above in the developer console, we can see we have access to the context of inner(y)
. Upon closer inspection, we can see that part its context is a [[Scopes]]
array, which contains all three scopes we were talking about.
Lo and behold, the array of scopes contains its parent function's scope, which contains x = 3
:
Common Use Cases
Closures are useful because they help us cluster data with functions that operate on that data. This might ring a bell to some of you who are familiar with Object-Oriented Programming (OOP). As a result, we can use closures anywhere we might use an object.
Another major use-case of closures is when we need our variables to be private, as variables defined in the scope of a closure is off-limits to the functions outside of it. At the same time, closures have access to variables in its scope chain.
Let's look at the following example to understand this better:
const balance = (function() {
let privateBalance = 0;
return {
increment: function(value){
privateBalance += value;
return privateBalance;
},
decrement: function(value){
privateBalance -= value;
return privateBalance;
},
show: function(){
return privateBalance;
}
}
})()
console.log(balance.show()); // 0
console.log(balance.increment(500)); // 500
console.log(balance.decrement(200)); // 300
In this example, we have defined a constant variable balance
and set it as the return value of our anonymous function. Notice that privateBalance
can only be changed by calling the methods on balance
.
Conclusion
Although closures are a fairly niche concept in JavaScript, they're an important tool in good JavaScript developer's toolkit. They can be used to elegantly implement solutions which would otherwise be a tall order.
In this article, we have first learned a bit about scopes and how they are implemented in JavaScript. We then used this knowledge to understand how closures work under the hood and how to use them.