paint-brush
Please Explain Closures!by@nwthomas
1,246 reads
1,246 reads

Please Explain Closures!

by nwthomasJanuary 6th, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

A closure is an invisible compartmentalization of variables that are accessible to your code at various levels (e.g. the global scope, local function scope) JavaScript does this automatically, so all we need to do is understand it. JavaScript has created multiple scopes for your code behind the scenes; there is a global scope with the global global variable and local scope accessible in them. The ability of closures to access all variables in scope at the time of the function's creation could be used later when the function is used elsewhere, even another file.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Please Explain Closures!
nwthomas HackerNoon profile picture

(This article is part of an ongoing series on soft skills and technical wizardry from Nathan Thomas, a software engineer working in San Francisco. Click here for the previous article in the series, "Building Your First GraphQL Server." Click here for the next article in the series, a piece called "Building Your First GraphQL Server.")

Starting From the Outside In

Explaining closures is a frequently-used JavaScript interview question, but many engineers still don’t fully understand how they work. There’s a fog that surrounds it, much like the feeling you get when you eat too much during the holidays. 🥧

When I first started learning closures, my first impulse was to close my laptop screen and go stare existentially at the local pond. But don’t worry; closures trip up a lot of people, but you’ll understand how they work and the concepts behind them by the end of this article.

I’m going to walk you through a really cool (and more importantly, simple) example. You'll get it in no time at all.

Grab your favorite coffee, apple cider, hot chocolate, or wintery beverage, and let’s get started. ☕️

"I'll use any excuse to buy a new backpack." - Michael Potts

Grab Your Backpack

Closures are an interesting concept. They are truly invisible in your code, and yet they are very real. If you've written any JavaScript, you've almost certainly used them without even realizing it.

If nothing else, that should give you comfort; if you're already accidentally using them successfully, half the battle will just be learning the ideas behind them to fill in the gaps of how they work.

At a very base level, a closure is an invisible compartmentalization of variables that are accessible to your code at various levels (e.g. the global scope, local function scope, etc.).

Each time you write a function, a closure is created for the scope available to it. JavaScript does this automatically, so all we need to do is understand it.

For instance, let's say we have the following code:

const globalVariable = 1;

function createClosure() {
  const localVariable = 2;
  return function() {
    return globalVariable + localVariable;
  }
}

const closure = createClosure();

console.log(closure()); // returns 3

This code will log the number

3
to the console. JavaScript has created multiple scopes for your code behind the scenes; there is a global scope with the
globalVariable
in it and local scopes with the
localVariable
and
globalVariable
accessible in them.

Here's the same code with comments included to indicate which parts are global and local scopes:

// global scope with globalVariable available
const globalVariable = 1;

function createClosure() {
  // local scope with localVariable and globalVariable available
  const localVariable = 2;
  return function() {
    // local scope with localVariable and globalVariable available
    return globalVariable + localVariable;
  }
}

const closure = createClosure();

console.log(closure()); // returns 3

Stop and think about what's happening here for a moment; the createClosure function can access both variables.

What if I told you that this ability of closures to access all variables in scope at time of the function's creation could be used later when the function is used elsewhere, even another file?

I know. Has science gone too far?

Let's find out. 🧪

A good traveler has no fixed plans and is not intent on arriving." - Lao Tzu

Counting with Closures

Here's the code we're going to be working through for the rest of the article. Go ahead and read through it. 👍

Don't worry if you don't understand how it relates to closures yet. Just get an idea of what code is actually doing:

function createCountingClosure(startingNum) {
  if (typeof startingNum !== "number") return null;
  let num = startingNum;
  function increment() {
    num++;
    return num;
  }
  function decrement() {
    num--;
    return num;
  }
  function getNum() {
    return num;
  }
  return [increment, decrement, getNum];
}

When we invoke that function, here's what we get out of it:

const [increment, decrement, getNum] = createCountingClosure(0);

Furthermore, we can invoke those

increment
,
decrement
, and
getNum
functions to get the following:

increment(); // 1
increment(); // 2
increment(); // 3
decrement(); // 2
increment(); // 3
getNum(); // 3

Whoa.

Don't worry. We're about to work through this code in detail to figure out exactly what's going on.

(Side note - We're doing some destructuring here. If you don't know what destructuring is, no worries. Check out MDN's excellent article on it here.)

Let's look at that

createCountingClosure
function again and break down each part:

function createCountingClosure(startingNum) {
  if (typeof startingNum !== "number") return null;
  let num = startingNum;
  function increment() {
    num++;
    return num;
  }
  function decrement() {
    num--;
    return num;
  }
  function getNum() {
    return num;
  }
  return [increment, decrement, getNum];
}

First, we're taking in an argument called

startingNum
. We are clearly expecting it to be a number (as the next line, an if statement, returns
null
if it isn't).

Next, we're assigning the value of

startingNum
to the
num
variable. This gives our counter function a starting value to work with and change later.

We then proceed to declare three functions:

  1. increment
    , which increases the value of
    num
    and returns its value
  2. decrement
    , which decreases the value of
    num
    and returns its value
  3. getNum
    , which returns the current value of
    num

Finally, we return these three functions inside an array. As functions are "first class citizens" in JavaScript, we can pass functions around like this to be returned from our function and used elsewhere.

This is why, later on, we can invoke createCountingClosure and destructure out these three functions:

const [increment, decrement, getNum] = createCountingClosure(0);

As you can see, we're also passing

0
into our function. Remember how we set it up to accept a
startingNum
argument? That value is going to be
0
in this example.

With all of that out of the way, we're now at the part of discussing the

createCountingClosure
function where we can talk about how the closures work.

Hold on to your butts.

What's in the Bag?

Let's take a look at the

createCountingClosure
function again:

function createCountingClosure(startingNum) {
  if (typeof startingNum !== "number") return null;
  let num = startingNum;
  function increment() {
    num++;
    return num;
  }
  function decrement() {
    num--;
    return num;
  }
  function getNum() {
    return num;
  }
  return [increment, decrement, getNum];
}

When we declared

increment
,
decrement
, and
getNum
, we unknowingly created little closure "backpacks" for each of them that store access to the all variables available in their scope when these functions were created.

Remember at the start of this article how we had global and local scope? Remember how the function we created had a local scope that retained access to the

globalVariable
in the global scope?

Well, the same thing is happening here. For instances, the

increment
function retains access to the
num
variable. In fact, all of these functions can still access this variable later on anywhere that we might use them (even if it's in a different file).

For example, remember how we destructured out all of these functions when we invoked

createCountingClosure
?

const [increment, decrement, getNum] = createCountingClosure(0);

Here's what the

increment
function has access to (in code) at the time it's destructured out as we did above:

let num = startingNum;
function increment() {
  num++;
  return num;
}

Even though we've only destructured out

increment
, it still has access to the
num
variable. It's in the function's closure backpack, along for the ride like a tiny little Yoda.

What this means is that we can access and modify our

num
variable even though we don't have it directly inside the function itself.

Isn't that wild? 🦁

This same thing is true for

decrement
and
getNum
, too. They both have access to
num
in the same manner through their closure from when they were created.

Let's go ahead and use our code to see how this works.

"I would gladly live out of a backpack if it meant I could see the world." - Unknown

Updating Your Backpack

We're going to use our code and walk through it step by step to observe what it does. Here's a refresh of the entire thing before we get started:

function createCountingClosure(startingNum) {
  if (typeof startingNum !== "number") return null;
  let num = startingNum;
  function increment() {
    num++;
    return num;
  }
  function decrement() {
    num--;
    return num;
  }
  function getNum() {
    return num;
  }
  return [increment, decrement, getNum];
}

In addition, we're going to invoke our

createCountingClosures
function and destructure out the functions returned in the array like we did previously:

const [increment, decrement, getNum] = createCountingClosure(0);

With this setup, we're now ready to go. If you want to play around with this while we walk through the rest of this article, here's a codepen link that will allow you to interact with the code.

First off, we're going to invoke the increment function:

increment(); // should return 1

As we've previously discussed, the

increment
function retains access to the num variable. When we invoke
increment
, the function will increase the value of
num
behind the scenes.

Since we passed in the value of

0
when we initially invoked
createCountingClosures
up above, we increment that value to
1
.

(If you're following along in the CodePen I provided, you'll see that pop up in the console because I've wrapped the function call in a

console.log
.)

Next, we'll invoke increment two more times like so:

increment(); // should return 2
increment(); // should return 3

Notice that the value being returned each time is not starting at

1
; this is because the same variable
num
that we used above is also retained by
increment
and modified each time it is invoked. This is the incredible power of closures!

Next, we're going to call the function

decrement
to decrease the value of
num
:

decrement(); // should return 2

Notice that the value is, once again, not starting at

0
. The functions
decrement
and
increment
both retain access to the same
num
variable and share the ability to modify it even though it is not contained inside either of them.

Finally, we're going to call

increment
one more time and cap it off by calling the
getNum
function (which just returns the current integer value of
num
):

increment(); // should return 3
getNum(); // 3

All of these function calls have access to the num variable as all of them have the reference to it stored away in their little scope "backpack."

The final

getNum
call returns 3, which is the value of
num
from all of the many changes we've made to it. Even though
num
is not contained directly inside any of the functions we've been invoking, we've modified it through the power of closures.

If you're still having some trouble grasping how closures work, here's an excellent short video from MPJ (who runs the Fun Fun Function channel on YouTube) about how closures work:

Conclusion

I hope that this brief overlook of how closures work helps you grasp them.

By imagining them as little backpacks that functions can wear as they move around your application, you can build a framework in your mind for how you can access and manipulate variables contained inside of closures.

Also, now that we've stepped through how they work, you can be confident in your closure knowledge and crush your next job interview.

Thanks for reading. 🔥

Nathan

(TwitterLinkedInGitHub, and Portfolio Site)