JavaScript Closures, and How They Work
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, when and 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/When are we meant to use closures?
Since closures form a core part of how functions handle scope, it's a mistake to think in terms of "using closures" and "not using closures". It's far easier to think of it in terms of using functions.
That being said, people talk about "using closures" - all they mean is that they use an inner function that is accessing an outer scope. A common example of one such 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
};
If you're looking up some keywords to find other common uses of closures, be sure to check out currying, and partial application.
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.