JavaScript Closures
Objectives
- Explain what a closure is
- Explain how a closure works
- Practice using a closure
- Describe use cases for closures in JavaScript
Introduction โ The Spy Who Ate Me
Let's talk about one of my favorite characters in the Austin Powers movies: Fat Bastard and his gluttonous appetite. If you haven't seen those movies yet, you should do so now. If it makes you feel better, pretend they're required viewing for this course!
In the movie, Fat Bastard wants to eat Mini-Me. Let's make this dream come true for our chubby assassin friend.
What's for dinner?
We'll represent Fat Bastard using a function that takes one argument, the thing he's going to eat:
function fatBastard(meal) {
}
Next, we'll add a way for Fat Bastard to tell us what he's having for dinner, by returning a function. We can then assign the returned function to a variable, and call it when we want to know what type of food we gave Fat Bastard for dinner.
function fatBastard(meal) {
function whatsForDinner() { // whatsForDinner() is the inner function, a closure
if (meal === 'Mini-Me') { // whatsForDinner() uses argument provided to the parent function
console.log('The wee man is in my belly!');
} else {
console.log(`I'm eatin' a bit of ${meal}! Burp.`);
}
}
return whatsForDinner;
}
whatsForDinner()
is an inner function of the fatBastard()
function, and as such, it has access to all of the variables
defined in its parent function (along with any variables in its own scope, if it has any). This means that we
can access the meal
argument in our whatsForDinner()
function. However, meal
isn't accessible outside of
the fatBastard()
function, giving us some semblance of 'private' variables. This is one possible use case for closures.
As you can see, we're not executing the whatsForDinner()
function here, we're merely returning it. We can then run
the whatsForDinner()
function at a later point in time, when we're curious about what exactly is in Fat Bastard's belly.
The reason whatsForDinner()
still has access to the variables within its parent function long after the parent
function has executed is because whatsForDinner()
is a 'closure'.
To prove that Fat Bastard can still let us know what is in his belly after he's eaten it, let's feed him a nice, juicy steak.
const whatsForDinner = fatBastard('Kobe beef');
whatsForDinner(); // prints 'I'm eatin' a bit of Kobe beef! Burp.'
Keep in mind that since we're returning a function as a value, there's no need to use the returned function's name as the same name for our variable. Meaning, this would work too:
const whatsInHisTummy = fatBastard('Mini-Me');
whatsInHisTummy(); // prints 'The wee man is in my belly!'
Gotchas
When a closure is created, its environment is remembered and not discarded. That means that the variables in that scope are shared (just like regular variables), and can also be changed. This is something you need to be aware of. To illustrate, let's expand on what Fat Bastard can do with his meal, by allowing him to digest it:
function fatBastard(meal) {
function whatsForDinner() { // whatsForDinner() is an inner function, a closure
if (!meal) { // whatsForDinner() uses argument provided to the parent function
console.log('My belly is empty. Woe is me.');
} else if (meal === 'Mini-Me') {
console.log('The wee man is in my belly!');
} else {
console.log(`I'm eatin' a bit of ${meal}! Burp.`);
}
}
function digest() { // digest() is an inner function, a closure
meal = undefined; // digest() uses argument provided to the parent function
}
return {
whatsForDinner,
digest
};
}
Keep in mind that we're now returning an object with references to our whatsForDinner()
and digest()
functions, meaning that now we do need to use the key names to refer to them! Let's serve him another slab of beef, and give him
some time to digest it:
const { whatsForDinner, digest } = fatBastard('ribeye');
whatsForDinner(); // prints 'I'm eatin' a bit of ribeye! Burp.'
digest();
whatsForDinner(); // prints 'My belly is empty. Woe is me.'
Note:
If you have never seen the syntax above const { whatsForDinner, digest } = fatBastard('ribeye');
don't be alarmed! That's a new ES6 feature called (Object Destructuring)[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring]. Basically, developers frequently found themselves having a large JS object such as a response from an API and they might want to declare several variables where the name of the variable is the same as the name of the key of the object. Example:
var firstName = response.firstName;
var lastName = response.lastName;
var url = response.url;
// ...
ES6 provides a shortcut for doing just this task! What do you think are the values of the variables a
, b
, and c
here:
var obj = {a: 1, b: 2, c: 3};
var {a, b, c} = obj;
Check out the documentation and experiment in the console.
Another thing to watch out for is that closures are the most common source of performance issues and memory leaks. Since the variables that are closed over might still be in use, they're either never or barely picked up by the garbage collection.
Note:
Garbage collection (GC) is basically something that happens in the background to automatically manage the memory used by our application. Stuff that's no longer being used by the program takes up unnecessary memory, and the GC is responsible for cleaning it up.
Garbage collection is something that you generally don't have to worry about, unless you're writing UI code that gets executed a lot (e.g. the people working on Angular, React, ... do keep an eye on performance).
Pirates and passwords
Modularization and abstracting away implementation details is key to writing good, clean code. For example, imagine you're in a back alley. There's a door on the left, with a shady looking character looking out the peephole. As you look at him, he yells out in a raspy voice. "Wot's the password?". He knows the password, but there's no way for us to obtain that information from him. He's certainly not going to tell us. Let's represent our new friend in code:
function raspyDoorGuy() {
const password = 'yarr'; // password is a local variable created by raspyDoorGuy()
function givePassword(givenPassword) { // givePassword() is the inner function, a closure
if (givenPassword === password) { // givePassword() uses variable declared in the parent function
console.log('Ye may enter.');
} else {
console.log('Begone, landlubber!');
}
}
return {
givePassword
};
}
Let's try to access the password
variable:
console.log(password); // prints `undefined`
Dang it. Well, it was worth a try. It makes sense though, since password
is enclosed within the raspyDoorGuy()
function,
but it's not returning it. There's no way for us to know what the right password is. Let's try to give him a password,
since that's something that our raspyDoorGuy()
function does return:
const { givePassword } = raspyDoorGuy();
givePassword('kittens'); // prints 'Begone, landlubber!'
The givePassword()
function can still access the password
variable defined in the scope of its parent function because givePassword()
is a closure. This is how the man behind the door can check if the password we gave is the right one. Unfortunately
for us, we still don't know the right password. What if we could bribe him? That would help us out. Since everyone has
a set of internal moral beliefs, we can abstract away that functionality. All we care about is if he thinks our bribe
is enough.
function raspyDoorGuy() {
const password = 'yarr'; // password is a local variable created by raspyDoorGuy()
function givePassword(givenPassword) { // givePassword() is an inner function, a closure
if (givenPassword === password) { // givePassword() uses variable declared in the parent function
console.log('Ye may enter.');
} else {
console.log('Begone, landlubber!');
}
}
function willBreakPrinciples(bribeAmount) { // willBreakPrinciples() is the private method
return bribeAmount >= 50;
}
function bribe(amount) { // bribe() is an inner function, a closure
if (willBreakPrinciples(amount)) { // bribe() uses private method created in the parent function
return password; // bribe() uses variable declared in the parent function
} else {
console.log("Pssht. That won't work.");
}
}
return {
givePassword,
bribe
};
}
If we take a look at the code above, we see that there are now three functions, but we only return two. This means
that the willBreakPrinciples()
function is private โ it can only be accessed by the two closures (givePassword()
and bribe()
) that share the same environment. This sort of emulates
private methods you see in other programming languages. There's no way for us to know how much a good bribe would be,
but if we give him enough money, we'll get the password:
const { bribe } = raspyDoorGuy();
bribe(80); // returns 'yarr'
And now we're in! That's about it for closures. It's important to know how they work and why they're useful, as well as their implications.
Resources
View Closures on Learn.co and start learning to code for free.