Closures are one of the most confusing concepts for new JavaScript learners. They are very powerful. Once you understand them well, your JavaScript skills will move to the next level.
In this article, I will give you all the knowledge you need to understand closures well. We will start with defining what scoping is and the different types of scoping. Then, we will learn the different ways to define variables in JavaScript. And we will move in small steps until we see how closures are defined. At the end of this article, I will show 5 different practical applications of closures that you can use now in your JavaScript code.
What is scoping?
In simple terms, scoping is the rules that determine how a variable (or function or class) is evaluated and accessed.
For example, we say that this variable's scope is some function or some if statement, and sometimes we say that its scope is global, meaning we can access it and modify it from anywhere.
Here are a couple of scoping examples.
// a is global variable
var a = 5
function fun() {
// b's scope is only within the function fun
let b = 'foo'
if (b === 'foo') {
// c's scope is only within this if statement
let c = 'bar'
}
}
Static (lexical) vs. dynamic scoping
We always hear that JavaScript is a lexical scoping language, but it's not clear what that means.
To understand what that means, we need to understand the alternative: dynamic scoping.
In some old languages, like Lisp and Bash, dynamic scoping was used. However, most modern languages stopped using it because dynamic scoping can lead to unpredictable behavior.
It can lead to that because variable scopes are determined based on the calling context at runtime. Let me make this clear with an example.
Take a look at this code:
let v = 10
// Called by b()
function a() {
return v
}
// b() has its own variable named as v and calls a()
function b() {
let v = 20
return a()
}
console.log(b())
If I asked you what value console.log(b())
will log, it will be an obvious answer: 10.
It's 10 because function a()
returns v
, and v
is defined above and has the value 10.
That's how static (also called lexical) scoping works. You can know the value just by looking at the code.
If dynamic scoping were what JavaScript uses, the logged value would be 20. It would be 20 because when running the code, the function b()
would override the scope used for the variable v
to the one defined in b()
, and that would make a()
use the last one, which is 20.
Thankfully, JavaScript doesn't use dynamic scoping and uses lexical scoping instead.
So you can simply think of lexical scoping as the scoping that allows us to know what variable will be used just by looking at the code at compile time (if the language is compiled) and not at runtime.
let
and const
vs. var
Advice everyone gets when learning JavaScript is to always use let
and const
instead of var
. But why is that? Is var
bad? Not necessarily.
It's all about scoping.
let
and const
are block-scoped, while var
is function scoped. What does that mean?
It means that any variable declared with let
or const
will be available only within the block it was defined in. By block, I mean {}
(curly braces).
This means this:
let a = 'foo'
{
let b = 'bar'
console.log(b) // Output: 'bar'
}
console.log(b) // Error: undefined
Look how I defined a block even without any additional keywords like if
or for
. That's a valid syntax, but in real-world code, the blocks are usually preceded with some keywords like when defining if statements, while loops, and functions.
This is why when we use let
and const
in if statements, for example, we can't use the variable outside of it.
if (true) {
// Here is a new scope (inner scope)
const a = 'foo'
}
// Here is the outer scope (another one)
console.log(a) // Error: undefined
So console.log
logged undefined
because it's in a different scope from the if statement. This means you can define the variable a
again in the outer scope without affecting the one in the inner scope.
if (true) {
const a = 'foo'
}
const a = 'bar'
console.log(a) // bar
So that's how let
and const
scoping works. Let's see how var
works.
var
is for defining function-scoped variables. This means it will create a new scope only if it is used within a function, like this:
function foo() {
var a = 'foo'
}
console.log(a) // Error: undefined
Note that var
is only function scoped (not block scoped), which means defining it within an if statement, for example, will define it globally (on the window
object if in the browser).
if(true) {
var a = 'foo'
}
console.log(a) // foo
So var
will define a new scope if it's used in a function; otherwise, it will define the variable in the global scope.
Thus, we get the advice to prefer let
and const
over var
because block-scoped variables are more predictable and easier to reason about.
Access direction
An important rule to remember in lexical scoping: inner scopes can access outer scopes, but not the other way around.
Here's an example:
let a = 1
// cannot access b or c
{
// can access a
// cannot access c
let b = 2
{
// can access a
// can access b
let c = 3
}
}
You can think of it as: you can't look inside things, but you can look outside them.
You need to always remember this rule as closures heavily depend on it as you will see next.
It also works for functions
Until now, we've been looking at examples that show how scoping works for variables. But the same is true for functions.
Let's take a look at this example:
function a() {
function b() {
console.log('from b')
}
b()
console.log('from a')
}
b() // Error: b is not defined
It errors because the function b
is not defined within the scope of the caller.
If we modify the example to call a()
instead, it will work because a
is defined within the scope of the caller.
function a() {
function b() {
console.log('from b')
}
b()
console.log('from a')
}
a()
// Output:
// from b
// from a
Also, note how it was able to call the function b
from function a
because b
is defined within the scope of a
.
In this example, we have nested functions, so we are close to the concept of closures (get ready).
Returning a function from another function
Let's take the previous example and modify it to return the function b
from the function a
.
function a() {
return function b() {
console.log('from b')
}
}
const nestedFunction = a()
nestedFunction() // Output: from b
Notice how we were able to overcome the restriction of scopes by returning the function. In the previous example, we weren't able to call the function b
directly in the outer scope, but when we returned the function b
, we were able to access it and call it.
Let's stop for a moment here to talk about the idea of returning a function.
When in a language a function can return another function (or take functions as arguments), we say two things:
- this language supports functions as first-class citizens
- and we call that function a Higher-order function (HoF).
This is why we say JavaScript supports functional programming.
One more step to create a closure
Let's take the previous example and make the function b
use (return) a variable defined in the function a
.