Introduction
A closure gives you access to an outer/enclosing function’s scope, including the variables from an inner function, even after the execution context of the outer scope has popped off the call stack. The above statement might sound ambiguous but you will see it in action soon.
The hallmark of learning about closure is the ability to identify closure and understand the rationale behind how they work and how they are created.
In this article, I will give a detailed explanation to understand how and why closure works and the pros and cons of it.
Prerequisite
To really grasp and understand the concept of closure, it is pertinent to understand first how execution context and the scope chain work.
This can be seen as a sequel to this scope and scope chain article. See this as the last dance to understanding everything about scope in javascript.
What is closure?
A closure is a function created inside a parent(outer) function that has access to the parent scope. It is commonly created when a function is returned from an outer function, but it can also be created without returning a function as we will see later on. A Scope chain defines the scope for a given function. When a variable not defined within the body of an inner/child function is called/used in that function, javascript checks the parent function and global scope accordingly for the value of that variable. In other words, it looks up the scope chain for the variable.
Consider the code example below;
function numOfPassengers() {
let passengersCount = 0;
return function () {
passengersCount++;
console.log(`total number of passengers is ${passengersCount}`);
};
}
Const increasePassenger = numOfPassengers();
increasePassengers();
increasePassengers();
In the code snippet, the return value of numOfpassengers()
function is a function that is passed into the ‘increasePassenger’ variable.
The ‘increasePassenger’ variable now becomes a function and a closure as well because it is defined in the numOfpassengers() function and has access to its variables. Subsequently, we can call the increasePassenger()
function and the result of calling the function is an increment in the value of "passengersCount" logged into the console.
Calling the increasePassenger()
multiple times will keep increasing the passengerCount variable of the numOfPassenger function, even when the numOfPassenger function is not executing or has already been executed, ordinarily, it isn’t supposed to be acheivable, you cannot access variables of a returned function or variables outside its scope but with closure we can do that .
To explain why it is possible, I'll take you back a bit to the execution context and progress from there, so enjoy the ride.
There are two types of execution contexts: The global and function execution context. When the javascript engine runs a script, it first creates a global context which contains all global variables and function declarations/expressions. When a function is called, a function execution context is created and put on top of the call stack, this creates a local scope and after the execution, it is destroyed and popped off the stack. Local variables, therefore, are only available to a function when it is executing. This is basic knowledge we are probably aware of from learning about execution context.
Closures unravel a different perspective to what we already knew about the accessibility of local variables outside the execution context. With closure, we can still access those local variables even though the function is done with execution and has already popped off the stack. That is possible because The scope chain allows an inner function to access the parent/enclosing function. However, because JavaScript is garbage-collected(this means the memory given to a scope is destroyed as it's popped off the stack) this shouldn’t be achievable outside the execution context. Still, it is possible, because a closure keeps a reference to its outer scope which preserves the scope chain throughout time.
Put differently, a closure always has access to the variable environment of the execution context it was created even when the function has returned or the execution context is gone.
In the previous code example, before we called the increasePassenger()
function, the numOfPassenger function returned and its execution context has been popped off the stack. while we ran the increasepassenger ()
function, the function could still access and manipulate the "passengerCount" variable of the increasePassenger() function because closure does not lose connection to the variables that existed while being created.
More Examples of Closure
I mentioned earlier that, one of the ways closures are created is by returning a function from a parent function, another way closure is achieved is by declaring a variable and then assigning it an inner function as seen below.
let addFunc;
const calcFunc = function () {
const a = 23;
addFunc = function () {
console.log(a + 2);
};
};
calcFunc();
addFunc();
The addFunc() function can now be referred to as a closure.
(function () {
const header = document.querySelector('h1');
header.style.color = 'red';
document.querySelector('body').addEventListener('click', function () {
header.style.color = 'blue';
});
})();
The snippet above is an example of an immediately invoked function expression(IIFE), IIFE is a function that is executed as soon as it is created. The parenthesis attached at the end of the function makes sure to call it immediately, hence the name. The callback function attached to the event handler is not called until the click event occurs which can happen long after the outer function executes when the script is run. Yet, it can still access the header variable and change the colour when clicked because it is a closure, therefore it doesn’t lose access to the variable that exited when it was created.
Looping with Closure
Using the setTimeout function and a var-declared variable in a loop, produce an unexpected and undesired outcome, to circumvent such behaviours we can convert the setTimeout to a closure.
consider the code snippet below;
for (var i = 1; i < 4; i++){
setTimeout(function (){
console.log('seconds' + i);
}, i * 1000)
}
In the code above, the loop runs three times and with each iteration, there is a setTimeout function that logs to the console “seconds” plus the value of i after a time period of i * 1000 in milliseconds.
Ordinarily, you would expect the output to be as follows;
"seconds: 1"
"seconds: 2"
"seconds: 3"
But what we will get is ;
"seconds: 3"
"seconds: 3"
"seconds: 3"
The var keyword is not block-scoped and so variables declared with var are not bound by curly braces or brackets. in the above loop, the var variable is scoped to the global environment. With each iteration, the value of the variable “i” is updated in the global scope, without a settimeout function we can immediately print the value of i to the console before the global scope is updated by another iteration, but with the setimeout function there is a wait for some millisecond before the callback is fired and by that time, the var variable has been updated to the maximum value, so when the callback of the first and second loops looks up the scope chain it gets the final value of i which is 3.
There are two ways to circumvent this behaviour.
The first is the use of immediately invoked function expression(IIFE).
for (var i = 1; i < 4; i++) {
(function (i) {
setTimeout(function () {
console.log('seconds: ' + i);
}, i * 1000);
})(i);
}
To preserve the state/value of ‘i’ with each iteration, you can create an outer function and execute it immediately, this function creates its scope and the value of ‘i’ in the scope is the value at that particular loop, remember the var is function-bound and so the global scope is not updated. the setTimeout
which is now a closure, waits a few milliseconds, use the value of “i” in the outer function scope at the time it was executing and at that particular loop, instead of looking up the global scope.
Use of ES6 “let” keyword;
A good practice for coding in javaSCript now is the use of ES6 syntax "let" for variables that can be reassigned. The let keyword is blocked-scope and so create a scope for each loop which has its own variables.
for (let i = 1; id < 4; i++) {
setTimeout(function () {
console.log('seconds: ' + i);
}, i * 1000);
}
With that, we can get the output we were expecting/desired.
Pros of closure
- Data Encapsulation:
Since global variables are accessible throughout the program, there are exposed to manipulations and changes from everywhere, which is why it is not advised to populate our script with global variables but rather create local variables for each function. These variables can be accessed by the closure throughout the scope chain.
- State Preservation:
Local variables are only available when a function is executing, however, with closure, we can still access these private variables after execution. Thus closures help minimise access to a variable while at the same time preserving access to local variables within the scope.
- Currying:
We can create an outer function that can be called with a single argument which returns a function that further accepts additional arguments. This is currying and it makes our code flexible and reusable because it makes possible a wide range of ways the functions can be called thus eliminating redundant code.
Cons of closure
Creating closure unnecessarily can adversely affect the performance of the script by:
Consuming excess memory: Closures are closed over variables of the function instance they are associated with, and this last as long as there is still reference to the outer function, the consequence of this is that closures are not garbage-collected and as such consumes memory space.
Slowing down the processing speed: with reduced memory, comes a decline in processing speed.
Summary
Closure is a tricky concept in javascript because it is not something we explicitly set out to create like say declaring a variable or creating a function, however, it is important to know and identify closure as a javascript developer.
Thank you for staying till the end!