visit
(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.")
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
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
“A good traveler has no fixed plans and is not intent on arriving." - Lao Tzu
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];
}
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
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:increment
, which increases the value of num
and returns its valuedecrement
, which decreases the value of num
and returns its valuegetNum
, which returns the current value of num
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.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
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);
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
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:Thanks for reading. 🔥
Nathan
(, , , and )