JavaScript Closures, and How They Work
Carl Anderson
Posted on January 17, 2019
Closures, as well as being a favourite interview question, are one of the more confusing parts of JavaScript.
Despite the wealth of articles offering explanations of closures, there are dozens of threads on reddit and other forums asking for more help with closures. It's not hard to see why, since most explanations of closures come out looking like this:
Imagine you have a backpack.
Inside that backpack there is a folder, a lunchbox, and some mail.
Inside the lunchbox, there is also an apple.
Suddenly, all of these items come to life and try to take stuff from each other, breaking this analogy.
Luckily, because of closures, some of them can't.
The end.
Closures are a fundamental part of JavaScript that every serious programmer should know inside out - and once you remove the lunch-based explanations - if you have a basic understanding of scope - mastering closures is easy!
This article is going to cover exactly what a closure is, how to use them, and why you should care.
What is a closure anyway?
To put a long story short: closures are functions.
That's it. Honestly. Obviously, they are a little bit more complex than that, otherwise we wouldn't have bothered to give it a special name.
The concept of closures exist because of one rule we have in Javascript: Inner scopes are able to access everything in parent scopes. Since functions create a new scope, this can become: "Every function has access to variables and parameters from its parent functions."
The detailed explanation is that closures are an implementation detail of Javascript – In order to ensure variables from parent scopes remain in scope, functions need to keep references to them. A closure is the combination of a function and the scope the function has stored.
What closures means for a developer is that any function I create will only reference the scope where it was defined, no matter where the function is actually called.
How are we meant to use closures?
The truth of closures is that you very rarely need to "use" them explicitly. They form a core part of how functions handle scope, so you tend to use them without ever needing to think about it.
A common example of a closure in action you might be familiar with is callbacks.
//foo.js
import {sillyConcat} from './bah.js';
const globals = {};
sillyConcat('hello', 'world' , function(result) {
//This function creates the closure, and includes a reference to globals
globals.hello = result;
});
//bah.js
function sillyConcat(s1, s2, callback) {
//This is where the closure is called - there is no direct access to
//variables from foo.js, but the function runs fine anyway
callback(s1 + s2);
}
export {
sillyConcat: sillyConcat
};
Why do we need to know about closures?
For the most part, you don't. Except when you do. It can be important to know how functions store references to variables in parent scopes to avoid bugs and some tricky gotchas.
This is a common gotcha that involves closures (and can be an interview question).
function delayedPrint() {
let total = 0;
for (let i = 0; i < 4; i++) {
total += i;
setTimeout(function closure() {
console.log(total);
}, 200);
}
}
delayedPrint(); //expected: 0, 1, 3, 6 actual: 6, 6, 6, 6
This happens because each of our setTimeout
functions takes a reference to the total
variable, but doesn't check its value. By the time the function is called, the loop has finished running, and total
equals 6 – so each function prints 6
.
To get around this, we need to copy the value of total
to a new variable that isn't stored in the parent scope. We can do this by passing it as a parameter to the function.
function delayedPrint() {
let total = 0;
for (let i = 0; i < 4; i++) {
total += i;
setTimeout(function closure(total) {
console.log(total);
}, 200, total);
}
}
delayedPrint(); //expected: 0, 1, 3, 6 actual: 0, 1, 3, 6
We could also achieve this by creating another function and calling it immediately (an IIFE).
function delayedPrint() {
let total = 0;
for (let i = 0; i < 4; i++) {
total += i;
(function(total) {
setTimeout(function closure() {
console.log(total);
}, 200);
})(total);
}
}
delayedPrint(); //expected: 0, 1, 3, 6 actual: 0, 1, 3, 6
If you're interested in learning more about closures, MDN has a great article.
If you found this article helpful, consider joining my newsletter.
Posted on January 17, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.