Developers from various stages of their careers will feel differently about closure. All the way from it's tricky, confusing, to what the heck!?!, it's unnecessary.
But is it though?
Closure is an interesting concept, it's one of those that almost morphs itself and slips into many practical uses, like in event handlers and callback functions, object data privacy and also in functional programming patterns like currying (psst... a post might be coming soon on currying!)
So let's try to understand this, widely perceived as tricky-concept, with some simple workable examples.
But first, quick highlights of some prerequisites:
1. Javascript's lexical Scope is the function scope.
Stuck at huh..? 🤷🏽♀️
Well, first off lexical scoping is simply a term for setting the scope of a variable so that it may only be called (referenced) from within the block of code in which it is defined. And in JS, a new lexical scope is created around a new function, i.e. function scope (bear in mind, scope creation happens at call-time) .
Consider this example:
const parentFunc = () => {
if(true) {
let firstVar = 10;
console.log(secVar); // ReferenceError: secVar not defined
}
const childFunc = () => {
if(true) {
let secVar = 5;
console.log(firstVar); // firstVar still known prints 10
}
if(true) {
console.log(secVar); // prints 5
}
}
return childFunc;
}
const myFunc = parentFunc();
myFunc();
In the code above, the variable firstVar
is available everywhere inside of parentFunc()
and variable secVar
is available everywhere within the childFunc()
, but neither are available outside of the function where they were defined. This in a nutshell, is lexical scoping for JS.
2. In JS, the search for a variable starts at the innermost scope and goes outwards from there.
If we pay attention in the example above, firstVar
isn't defined inside of the childFunc
but is still know there. This is because when JS doesn't find a variable in it's local/inner scope, it traverses outwards, in this case to parentFunc
, where it finds the definition of firstVar
and life's good!
So now, what is Closure?
According to the MDN Almighty, closure is a pattern in which functions can be bundled together along with references to it's surrounding state.
Let's expand on this with a simple example:
const parentAlerter = () => {
const firstVar = "Counting on closure:";
let count = 0;
const childAlerter = () => {
alert(`${firstVar} ${++count}`);
};
return childAlerter;
};
We have two functions here:
- the outer/wrapper function
parentAlerter()
which has two variables,firstVar
andcount
, and returns thechildAlerter()
function. The scopes of both thefirstVar
andcount
are limited to theparentAlerter()
function. - the inner function
childAlerter()
that alerts with the value of variablesfirstVar
andcount
within its function body.
Let's add to this example further, and see how closure and lexical scope can work together:
const myAlertOne = parentAlerter();
const myAlertTwo = parentAlerter();
myAlertOne();//line 3,references the first execution context created
myAlertOne();//line 4,also references the first execution context created
myAlertTwo();//line 5,references the second execution context created
- We invoke
parentAlerter()
twice and store thereturn
values of each of those invocations in two different variablesmyAlertOne
andmyAlertTwo
. One important thing to note here is that by doing the above, we created two different execution contexts forparentAlerter()
, one referenced by themyAlertOne
and the other bymyAlertTwo
. Now with that in mind we move forward. - Because the
return
value ofparentAlerter()
is the function body ofchildAlerter()
,myAlertOne
andmyAlertTwo
contain two separate copies ofchildAlerter()
. - When we call
myAlertOne()
on the third line, we enter the first execution context we created and see an alert with the following:Counting on closure: 1
. Note here that as we would expect after learning about lexical scope, thechildAlerter()
has access to variablesfirstVar
andcount
fromparentAlerter()
. - On line 4, when
myAlertOne()
is invoked again, it references the same execution context as the firstmyAlertOne()
call on line 3 and hence points to the same copies of variablesfirstVar
andcount
, and we see the output as an alert with:Counting on closure: 2
- On line 5, we are now working with the second execution context we created on line 2. This is a brand new copy and has not been affected at all with the workings of the first execution context. And so when we invoke
myAlertTwo()
, we see an alert with:Counting on closure: 1
.
Create your own (closure) magic:
There are some simple steps to follow with which you can create your own closure functions.
Let's dissect and understand their anatomy:
const parentFunc = () => {
let parentVar = "I am";
const childFunc = () => {
let childVar = "a pro at Closure now!";
console.log(parentVar+childVar);
}
return childFunc;
}
Step 1. First off, define a parent/wrapper function. Here we have parentFunc
.
Step 2. Define any number and type of variables that you desire, in the parent function's scope. These will be the variables we wish the child function to have access to. For example, we have the variable parentVar
above.
Step 3. Define a child function inside of the parent function's scope. We called it childFunc
.
Step 4. And lastly, return
that child function at the end of the parent function, so we return childFunc
.
And viola! You have your own closure functions, genius.
And now for some... Gotchas!
So hopefully by this point in the article you are feeling like a budding closure ninja ready to conquer the world of JS. But let me make you aware of some last few gotchas.
1. Scope can try to trick you.
Consider the example below:
const superPower = () => {
const ninja = () => {
console.log(who);
};
let who = 'I am a closure ninja!';
return ninja;
};
const aceClosure = superPower();
aceClosure();
When we invoke aceClosure
on the last line, the console logs I am a closure ninja!
. The thing to note here is that even though visibly the variable who
is defined after the function ninja
, we get no errors and see the desired output.
As mentioned under the prerequisite section earlier, javascript searches for a variable in the inner/self scope and goes outwards from there. In this case, we look for who
in the ninja
scope where it's not found, so the parent function, in this case superPower
's scope is searched next, and who
is found.
Takeaway: Think in terms of function scope instead of just linear parsing.
2. Class aren't the only way to abstraction in Javascript.
Let's dive into this example:
const myTimer = () => {
let elapsed = 0;
const myStopwatch = () => {
return elapsed;
};
const increment = () => elapsed++;
setInterval(increment, 1000); // line 10
return myStopwatch;
};
let timerInstance = myTimer(); // line 15
timerInstance();
- When we call
myTimer
in line 15, an execution context is created and thereturn
value ofmyTimer
, i.e., the function body ofmyStopwatch
is stored intimerInstance
. Because we invokedmyTimer
, thesetInterval
on line 10 starts to run. - Invoking
timerInstance
(in the last line) in turn invokesmyStopwatch
which has toreturn
elapsed
. elapsed
is searched for inside ofmyStopwatch
but isn't found, so we start traversing outwards. An instance of elapsed is found under the parent, i.e.myTimer
.- From step 1 above, the
setInterval
has been running all this while, which makes calls toincrement
and in turn the value stored inelapsed
keeps increasing by 1, with every passing second.
Note here that we only have access to timerInstance
which invokes myStopwatch
, and this function is completely oblivious to how the increment mechanism of elapsed
works. In other words, the timerInstance
or the myStopwatch
functions are abstracted from the increment mechanism.
Here is the output on the console for your reference:
Takeaway: Getting a good grasp on closure functions can help you implement abstraction without using classes.
And so...
To conclude, I will say what I do in all my posts of this series, javascript concepts can seem strange and sometimes even, unnecessary. But hopefully my writing invokes😉 your curious mind and you see some interesting possibilities with these concepts. Hopefully you'll give closure a chance, and create something fun. Happy coding!