Closures are one of those topics that will probably pop up in your JavaScript interviews. Newer developers are usually scared of them (I know I was), but there's no reason to be. Like recursion, closures can be over-explained in a way that makes them sound complex, yet at their core they couldn't be simpler. I'll prove it by writing one in 7 lines.
A simple closure
8
Boom. That's all there is, since:
A closure is when a function uses variables from a higher scope.
If you want to be super specific: "A closure is the combination of a function bundled together (enclosed) with references to its surrounding state" -mdn. But what I said means the same thing. Also "A function forming a closure" and "a closure" are used interchangeably in common speech, so I'm just going to call the function a closure to save time.
That inner function, runClosure
is the closure in this case, because it accesses variables from the outer scope without exposing them. That “exposed” part is a big reason why we even use closures. By only exposing the function, and not the variables it references, we essentially make them private. But before we talk about what closures are for, let's make sure we understand what we wrote.
Why aren't those arrow functions?
Of course closures work with both arrow and declared functions, however arrows are anonymous. In this article, where we'll be printing out function definitions, declared functions and their names are a little more illustrative. However, it would be a good challenge for you to convert all the named functions to arrows in your own notes!
Breaking it down
Of these two functions let's look at setupClosure
first. In this function's scope, three things happen:
- we define a variable x to be 2
- we define a function called
runClosure
- We return the
runClosure
definition
Returning it is the “weird” part since we don't normally return function definitions, we just call them. If we call setupClosure
, you can see that the value we get is the runClosure
's definitions:
ƒ runClosure(y) { console.log(x * y) }
By the way, you can use any console you like, but I recommend using Chrome's console to work with the examples since it has by far the nicest printing.
Anyway, you may have seen something like this before from a typo. A function's definition is the return value when we forget to actually invoke it with ()
:
ƒ example() { return "My example value" }
Calling the closure
So if the only difference between returning a function's calculated value or its definition is the (), what happens if we add another ()
?
8
As you saw before, we call the runClosure
function. Here's some pseudo code to illustrate the relationship:
That double ()
looks weird though, so normally people assign the setup function to a new name like this:
10
That reads a little cleaner, and it also lets us have another chance to describe the closure function with another variable name. This is a nice way to get a nice code readability boost.
What about x?
One of the best parts of closures are those inner variables, those enclosed values, because we have values that no other part of our program can touch. This is the power of closures: it lets us use data that can't be altered or accessed by any functions other than the closure itself.
The reason this works is because scopes can access values in their parents, but not their children. x
is defined in the same scope as runClosure
. That means the body of runClosure
is a child scope, so it can see x
and multiply it by y
. However, the only thing that gets exposed to the global
scope is the runClosure
function definition. Therefore, the only way we can interact with x
is by using runClosure
.
Avoiding accidents
Look at this counting function that uses a closure to grab a value in a higher scope, except the counter isn't protected:
No good right? Now let's use a setup function to protect our closured variable:
What's important to notice here is that 1) our value is no longer global
, so it's protected, and 2) the parameters of one function still count as a higher scope to a child function.
As you can see the basics of closures are simple: they rely on function bodies being able to reach up the scope chain, but not down. These sorts of closures that are only for privatizing a variable aren't super common though. What's a little more common is hitting closures when defining helper functions. So let's look at an example of that before we go.
Real world examples
Here we have a piece of code from a multiplication table app. You can click on buttons to see the multiplication results of a multiplier. The multiplier is saved on each button, and then when a user clicks on that, we display all the different values on a table row.
This code is already using a closure! Did you spot it? That's right, it's the forEach
callback with multiplier
. Remember, if a function ever references a variable that wasn't passed in, that function is forming a closure.
Let's refactor this code. We only reference cells
once, what if we call forEach
directly on the query result? And to keep our line length down, let's define an updateTableRow
function and pass that to forEach
:
ERR: multiplier is not defined
That no longer works because we broke our closure. updateTableRow
isn't defined in the same scope as multiplier
anymore. Sometimes, there are so many closured variables in a function, that the easiest thing to do is just define it in the same scope on a new line:
That's better than an inline function, but we can't reuse updateTableRow
in any other function. We can do better than that here. Remember our parameter trick from before? Well, we can use that here:
Here we pass in the multiplier
to updateTableRow
. Then, the actual callback for forEach
is the function definition that gets returned, which of course has access to multiplier
through num
! This is a super handy technique because we can share updateTableRow
to any other forEach
we like. We're simply taking the closured value and moving it from one parent function to another.
Beware of side effects
You've heard of "pure" functions, where the same inputs will always yield the same output, right? Well, closures break that, so be careful. By its very definition, closure means that the input arguments of the function aren't the only pieces of data it's pulling in. Whenever you use closures, make sure the data that winds up affecting the function is exactly what you think it is.
Another tool in your belt
Closures can come in handy in your code, they are an excellent concept to internalize in case they come up in an interview. Play around with them a bit in your next project, and look out for where you may have unknowingly already defined them (probably in your array functions).
Happy coding everyone,
Mike
Join the conversation on Reddit to leave a comment!