The Kraken's Lair: A JavaScript Closure Blog
Unlocking the Secrets of JavaScript Closures in The Kraken's Lair
Introduction
The closure is the most esoteric concept of JavaScript. Many JavaScript design patterns including the module pattern use closure. We can build iterators, handle partial applications and maintain a state in an asynchronous world.
But before diving into the concept of closure, let's again take a look at how the function works,
function multiplyBy2(input){
const result = input*2;
return result;
}
// New memory gets created during both function calls in their own execution context
const output = multiplyBy2(7);
const newOutput = multiplyBy2(10);
console.log(output); // 14
console.log(newOutput); // 20
When a function gets invoked a new execution context gets created with its own local memory and after the execution, this memory gets deleted. And it doesn't remember the previous running of function and data stored in local memory.
Functions with Memories
When our function gets invoked, we create a live store of data for that function's execution context. Another name for local memory is Variable Environment. When execution is finished local memory gets deleted except the returned value.
But what if our functions can hold on to live data between executions? Something like persistent memory. Let's keep in mind we can return a function from another function.
Returning Function
Let's create a function that can return another function as a value.
function createFunction(){
function multiplyBy2(num){
return num*2;
}
return multiplyBy2;
}
const generatedFunc = createFunction();
const result = generatedFunc(3);
console.log(result); // 6
Let's see the thread of execution:
Line 1: We are defining a function called
createFunction
and the code of the function is bundled up and stored in the global memory.Move to Line 8: We declared a constant named
generatedFunc
and assign it the value returned by the function call.Line 9: We declared a constant named
result
and assigned it the value returned bygeneratedFunc
.Line 11: We are outputting the returned value in the web console.
The execution context gets deleted as soon as the return keyword is encountered. We can return a function from another function because JavaScript functions are first-class objects. Now, back to our program.
In line 12, we are declaring a constant named result
and assign it the value returned by calling generatedFunc
.
This function has no relationship with createFunction
function. generatedFunc
contains returned value from createFunction
but it is not related to createFunction
.
Nested Function Scope
A nested function can inherit the arguments and variables of its containing function. The inner function contains the scope of the outer function. Let's see an example,
function outer(){
let counter = 0;
function incrementCounter(){
counter++;
console.log(counter);
}
incrementCounter();
}
outer(); // 1
outer(); // 1
outer(); // 1
outer(); // 1
Line 1: We are declaring a function named outer
in the global context.
Move to Line 11: We are calling outer
function 4 times.
But we can see every time we invoke outer
the function, the counter gets incremented by 1 and outputs 1 to the web console. This is because every time function is called a new execution context gets created with new local memory.
The below figure shows the thread of execution,
Retaining Function Memory
We can also create functions that can retain the memory of its execution context. Let's modify the above example a bit.
function outer(){
let counter = 0;
// function to increment counter
function incrementCounter(){
counter++;
console.log(counter);
}
return incrementCounter; // returning a function
}
const myNewFunction = outer();
myNewFunction(); // 1
myNewFunction(); // 2
In the above program, we are also incrementing the counter
variable but we are returning the incrementCounter
function from outer
function instead of calling it.
Now, let's keep some points in mind:
When function A gets returned from function B as a value, there is no relationship between A and B.
When we invoke a function, this function is pushed onto the call stack and a brand new execution context is created with its local memory.
Local memory gets destroyed when the execution is finished and we cannot access it again.
So, now the big question is counter
variable is in the local memory of outer
function and incrementCounter
function is being used to increment this variable. But if outer
function has no relationship with incrementCounter
function then how can we still able to increment this counter?
Now, this is where the concept of Closure comes in!
In the next line, we are invoking myNewFunction
.
The counter
variable is missing from both the local and global memory. So, we should get some kind of error in our terminal. But wait, it's still working and we are still getting output in our web console.
This is because when we returned incrementCounter
function from outer
function, we got more than the function definition. It took all its surrounding data present in its local memory and ships it.
This additional data is present in something we are going to call a "Backpack". So, now we have a function definition and also the associated local memory.
But how the function grabs the surrounding data and get it in the bag and ship with it? JavaScript gets a hidden property that links to where all this surrounding data is stored, this property is called [[scope]]
.
We cannot access this hidden property. The only way to access this data is to run this function, look for something that is not in its local memory then look for it in this hidden data block.
myNewFunction
can access counter
variable present in the backpack. The data present in the backpack is persistent. This is how memory-retaining functions can be built.
Closure Technical Definition
A closure is the combination of a function bundled together with references to its surrounding state. The local memory is also called Variable Environment. Let's see some other names for our backpack:
Closed over variable environment code
Persistent Lexical Scope Reference Data
JavaScript is a lexical scope or static scope language. In other words, where I save my function determines for the life of that function, what data it gets whenever it runs, whatever new label it gets, and what data will it have access to when the function runs.
Practical Applications
Some applications of closure are:
Closure gives us memory-persistent functions.
When can build helper functions like once and memoize.
We can build iterators and generators which use lexical scoping and closure to achieve contemporary patterns for handling data in JavaScript.
Module Pattern which helps in preserving the state for the life of an application without polluting the global namespace.
Asynchronous JavaScript
Conclusion
In this blog, we have seen
How to return a function from another function
How nested function scope works
How we can build memory-persistent functions using the concept of closure
Some practical applications of closure
If you're having any doubts please mention them in the comments below! ๐