Closure is notorious as one of the hardest concepts for Javascript beginners to pick up. That would be fine if it surfaced only rarely or in advanced applications. But closure plays a huge (and under-appreciated) role in Web Development 101. If you've ever added an event listener to handle a button click or a form submission like so: myElement.addEventListener('click', callbackFunction)
, you've likely been leveraging the power of closure.
Let's dive in.
Introducing closure
Closure is a hidden superpower of functions in Javascript and other programming languages. When thinking about closure, the mantra that I repeat to myself over and over again is:
Functions retain access to variables in the context in which they were created.
Functions retain access to variables in the context in which they were created.
Functions retain access to variables in the context in which they were created.
...
Set aside the technical details for a second. More on those to come. One of the biggest practical takeaways, especially for the budding developer, is that functions can, if created in the right context, access data that might seem "lost." You may have neglected to store a piece of data by assigning it to a global variable, but it's possible that one of your functions did not.
The killer use case for this in basic web development is achieving front-end persistence without a lot of awkward global variables. But first let's go deeper into what closure is.
A non-web example of closure
A classic simple illustration of closure begins with function that returns another function.
let count = 0;
function outer(a){
function inner(){
count++;
console.log(count)
return a * count;
}
return inner;
}
const ourFunction = outer(5)
ourFunction() // =>1
ourFunction() // =>2
ourFunction() // =>3
Here we are defining a function outer
that itself defines a function called inner
and returns it. inner
increments the global variable count
, console.logs it, multiplies it by the value of the parameter a
, and finally returns that result.
When we call outer
with the argument 5, it defines a function in the global scope named ourFunction
. So ourFunction
returns the next multiple of 5 each time it is called. Moreover, repeatedly calling ourFunction
will log the value of count
incremented by 1 each time. So ourFunction
seems to be doing a useful service not only of counting by five but also of tracking the number of times it has been invoked.
All of this should be relatively straightforward. Since count
is a global variable, it can be accessed from inside outer
and for that matter from inside inner
as well!
But count
is maybe a little too accessible. What if we had some friends who wanted to use outer
to do their own counting by 7s. We could try something like this.
let count = 0;
function outer(a){
function inner(){
count++;
console.log(count)
return a * count;
}
return inner;
}
const ourFunction = outer(5)
ourFunction() // =>1
ourFunction() // =>2
ourFunction() // =>3
const theirFunction = outer(7)
theirFunction() // =>4
theirFunction() // =>5
theirFunction() // =>6
ourFunction() // => 7
The global variable count
now gets incremented by both ourFunction
and theirFunction
. The first time theirFunction
gets called it logs 4 and returns 28. We have lost the desirable feature of counting up smoothly by 5s or 7s and of tracking the number of times each specific function was invoked.
What we would like to have is a count
variable specific to ourFunction
and another count
variable specific to theirFunction
.
We could define two global variables, ourCount
and theirCount
. But what if we wanted to count by 9s, 11s, 13s, and so on? We could create a new global variable each time, but then we could wind up with hundreds or thousands.
What to do?
We know from Javascript 101 that invoking a function creates its own execution context. Variables defined in that context are not accessible once the function returns a value: if you declare myVar
within a function, you don't then get to access myVar
outside that function. But this seems to deepen the mystery. Shouldn't a variable be either global (which won't work for us) or else confined to fleeting execution context where we can't get to it?
Let's go back to our mantra:
Functions retain access to variables in the context in which they were created.
Unpacking this, we know:
-
ourFunction
retains access to variables in the context in which it was created. - It was created in the execution context of the function
outer
. - So any variable declared in the scope of
outer
would be accessible toourFunction
.
Wouldn't that same variable be accessible to theirFunction
as well? Well, no, not the same variable, but a similar variable with the same name, because each time outer
is invoked it defines a new execution and count
is declared anew in that context and functions declared in the same context would have access to it! This seems to be exactly what we want.
This line of thought suggests that we simply declare count
inside outer
.
function outer(a){
let count = 0;
function inner(){
count++;
console.log(count)
return a * count;
}
return inner;
}
And indeed this gives us what we want.
const ourFunction = outer(5)
ourFunction() // =>1
ourFunction() // =>2
ourFunction() // =>3
const theirFunction = outer(7)
theirFunction() // =>1
theirFunction() // =>2
theirFunction() // =>3
ourFunction() // => 4
Each time we invoke outer
to create a function, we are also creating a locally-scoped count
variable that inner
, which is created in the same context, can access. We are creating a count
variable that is enclosed within that function.
Achieving front-end persistence
This may all seem like a curiosity or a programming exercise. How often do we actually define functions within functions?
It turns out to be surprisingly common. Any time you have a set of elements on a website--images, buttons, names in a list--and want to add the same functionality to each one, you are like to define a function that iterates over each element. And within that function you are like to call the addEventListener
method which itself takes a function, the so-called callback, has an argument. So for each element, you are declaring a function to be executed in response to a certain event, and you are doing that from within another function. Exactly the situation where closure can shine!
For example, what if you wanted to be able to track clicks or other events the same we tracked the number of times ourFunction
was invoked? It should be no surprise that closure can do the job.
A simple voting app
To make this more concrete, let's consider a simple website for tracking votes (clicks).
Note: We're dealing with front-end persistence only here, so "tracking" for our purposes means tracking within a single session. Any data will disappear when the page reloads.
Here the user can select three color options for which to vote as many times as they wish.
When you click the lightpink button, the background changes to lightpink and you can click the voting button to vote for lightpink. A counter of the current votes is displayed.
Similarly, when you click the lightblue button, the same thing happens. The background changes and you can now vote for lightblue and see a count of votes for lightblue.
Ditto for lightgreen.
Now the question: how do we get the vote counts for each color to persist such that votes for each color are counted accurately no matter how many times we switch between colors, voting as we wish for each? Three votes for lightgreen should stay three votes when I switch to light green, no matter how many times I vote for lightpink or lightblue in the interim.
Answer: closure.
The problem is really not so different from tracking invocations of ourFunction
and theirFunction
.
Code
Code for this section is on github
First we can create the bare bones of the voting website by iterating over an array of colors and calling a function on each.
const colors = ['lightpink', 'lightblue', 'lightgreen']
colors.forEach(addColorToDOM)
We simply need to define the addColorToDOM
function.
The function needs to do three things:
- Add each color button to the page
- Add functionality that changes the background color and displays votes in response to a click on the button
- Counts votes (i.e. clicks on the voting button) correctly
The first item is straightforward. For each color we simply generate a new DOM element, set its text content, and append it to the correct HTML element, which we've assigned to the variable colorList
.
// DOM selectors
const colorList = document.querySelector('#color-list')
// Render functions
function addColorToDOM(color){
const choiceButton = document.createElement('button')
choiceButton.textContent = color
colorList.append(choiceButton)
}
The second is a bit more complicated but still pretty easy. We simply add a click event listener to our choiceButton
element.
// DOM selectors
const colorList = document.querySelector('#color-list')
// Render functions
function addColorToDOM(color){
const choiceButton = document.createElement('button')
choiceButton.textContent = color
colorList.append(choiceButton)
choiceButton.addEventListener('click', () => {
document.body.style.backgroundColor = color
})
}
Nice.
The click also needs to display the votes, so let's go ahead and declare a voteCount
variable initialized to 0 and inject that content into the appropriate DOM element as well.
// DOM selectors
const colorList = document.querySelector('#color-list')
const votes = document.querySelector('#votes')
// Global variables
let voteCount = 0
// Render functions
function addColorToDOM(color){
const choiceButton = document.createElement('button')
choiceButton.textContent = color
colorList.append(choiceButton)
choiceButton.addEventListener('click', () => {
document.body.style.backgroundColor = color
votes.textContent = `Current votes for ${color}:
${voteCount}`
})
}
So far, so good. Now we have only have the third task of figuring out how to set voteCount
correctly for each color.
As a first step, observe that each color should listen for clicks on the vote button, but only count votes if that color is the currently selected color. So we'll need to add an event listener on the voteButton
element, plus a variable to track which color is currently selected
// DOM selectors
const colorList = document.querySelector('#color-list')
const votes = document.querySelector('#votes')
const voteButton = document.querySelector('#vote-btn')
// Global variables
let voteCount = 0
let currentSelection
// Render functions
function addColorToDOM(color){
const choiceButton = document.createElement('button')
choiceButton.textContent = color
colorList.append(choiceButton)
choiceButton.addEventListener('click', () => {
currentSelection = color
document.body.style.backgroundColor = color
votes.textContent = `Current votes for ${color}:
${voteCount}`
})
voteButton.addEventListener('click', ()=>{
if(currentSelection === color) {
voteCount++;
votes.textContent = `Current votes for ${color}:
${voteCount}`
}
})
}
We also need to modify the choiceButton
callback to update the currentSelection
variable. I've gone ahead and included those modifications in the code above as well.
OK. It looks like we have all the pieces we'll need. Let's go ahead and test.
I can easily add votes for lightpink.
But as soon as I select lightblue the counter starts where lightpink left off and increments from there. FAIL.
Hmm... we set up event listeners on each button with callbacks that increment the same global variable voteCount
. What we actually need is a voteCount
specific to each.
This is exactly the situation we faced with ourFunction
and theirFunction
. There too we had a count
variable that both functions could access, and we needed a variable that was specific to each. We solved the problem by declaring the count
variable in the unique execution context where each function was created and let closure do its thing.
Let's do the same here. We simply move the declaration of voteCount
from the global context to inside the addcolorToDOM
function.
// DOM selectors
const colorList = document.querySelector('#color-list')
const votes = document.querySelector('#votes')
const voteButton = document.querySelector('#vote-btn')
// Global variables
let currentSelection
// Render functions
function addColorToDOM(color){
const choiceButton = document.createElement('button')
choiceButton.textContent = color
colorList.append(choiceButton)
let voteCount = 0
choiceButton.addEventListener('click', () => {
currentSelection = color
document.body.style.backgroundColor = color
votes.textContent = `Current votes for ${color}:
${voteCount}`
})
voteButton.addEventListener('click', ()=>{
if(currentSelection === color) {
voteCount++;
votes.textContent = `Current votes for ${color}:
${voteCount}`
}
})
}
Now, when addColorToDom gets called on a color, it creates a new execution context and a new voteCount variable. And we know from our mantra
Functions retain access to variables in the context in which they were created.
that the callback functions created in the same execution context will retain access to those variables, even when the execution context goes away.
When we test out the solution, it does indeed work to count votes for each color and persist them when you switch back and forth.
I can start logging votes for lightpink:
Switch to lightblue:
And to lightgreen:
Vote counts are accurate and remain so even when I toggle back and forth to my heart's content.
Takeaway
We were able to achieve front-end persistence without any data structure that explicitly logged votes for each of the three colors. Instead, we were able to enclose a color-specific voteCount
variable within each callback function. Every time the callback function got invoked it retained access to its own voteCount
variable.
In fact the connection between closure and event listeners goes even deeper than what I've discussed here, but I'll leave that for another post.
Top comments (1)
hi! Awesome explanation. Please note the link is giving 404 status. thank you.