If you’re a JavaScript developer, you’ve likely heard of closures, but perhaps you’re not entirely sure what they are or how they work. In this post, we’ll delve deep into the fascinating world of closures and unravel the inner workings of JavaScript functions.
In this blog post, we will take a deep dive into how JavaScript functions really work. We’ll discuss concepts like higher-order functions (HOF), execution context, and lexical environment.
By the end of this journey, you’ll have a solid understanding of closures and their pivotal role in JavaScript development. Get ready to unlock the power of closures and take your JavaScript skills to the next level.
Under the hood, a function in JavaScript is actually an object. When a function is defined, JavaScript creates an object that holds the code of the function and additional properties like name
and length
.
This object is stored in memory and can be referenced and manipulated like any other object in JavaScript.
Furthermore, functions in JavaScript can be assigned to variables, passed as arguments to other functions, and returned as values from functions. This concept, known as functions being first-class citizens
, allows for powerful functional programming techniques.
Functions can be dynamically created, modified, and executed at runtime, providing flexibility and expressive power.
I have added this function named myFunction to the window global object in the browser, and we can see in the console that it’s just an object with special properties and methods like : name
, length
,call()
…etc.
This is just a quick overview of Function in JavaScript to get an idea and helps us go to the next step.
After talking a bit about Functions under the hood, we will discover Higher order function
, which is crucial to understanding Closures. From this point forward, we will refer to them as HOF for the rest of this blog.
A simple definition of HOF is that it is a function that can either take another function as an argument or return a function as its result.
Now, let’s begin with an example that does not involve the use of HOF :
/* In my code I want a Function that take an array
of numbers and multiply by that number */
const multiplyByNum = (arr, num) => arr.map(item => item * num);
/* In another place in my code I need a function takes
and array of numbers and devide them by a given number */
const devideByNum = (arr, num) => arr.map(item => item / num);
We can see that in this approach we are writing duplicate core. The only difference between the two functions is the arithmetic operations. Imagine we have 10 other operations for that array. How much will we duplicate that code?
This is breaking one of the main principles of Software Engineering (DRY) Don’t repeat yourself.
Here is where the (HOF) superpower comes into play :
// We define our (HOF)
function transformArrayByNumber(array, number, operation) {
return array.map((item) => return operation(item, number));
}
/* That's All this is a generic (HOF) function
that can do any operation we want not just multiplication and division */
const myArray = [0,1,2,3];
const multiply = (item , number) => item * number;
const multiplyByNum = transformArrayByNumber(myArray, 5 , multiply)
console.log(transformArray) // [0,5,10,15]
// this is how we can call our (HOF) function
As you can see, it gives us more flexibility and makes the code cleaner, and now, we can control our function operations only by changing the third argument operation
.
When the JavaScript Engine scans the JS file for the first time, it creates environments called Execution contexts
, and we have two types of Execution context
Global and Function. The Global is the first one that gets created when a JavaScript script first starts to run, and the Function is created whenever a function is called.
And to keep track of all contexts, JavaScript runtime uses a Call Stack in this way when first it creates the Global Execution context and pushes it on the stack.
Whenever a function is invoked, similarly, the JS engine creates a function stack context for the function and pushes it to the top of the call stack, and so on.
function firstFunction(string) {
return string;
}
function secondFunction(string) {
firstFunction(string)
}
function lastFunction(string) {
secondFunction(string)
}
consnt result = lastFunction("Hello Nadjem")
console.log(result) //"Hello Nadjem"
Let’s make a diagram to demonstrate how the above script is executed:
We can see, by following the diagram steps, how the Execution contexts get added and executed on and from the Call Stack.
Every Execution context
has its own Lexical Environment
; this Environment is a hidden associated object that contains two parts:
1 — Environment Record: It’s a location where variables and functions declarations are stored (and some other information like the value of this
). It’s located in the memory heap of the JS engine.
2 — Reference: To the outer Lexical Environment
; the one associated with the outer code.
I will make a Diagram right now for the previous code example with a bigger picture by including Execution context
and Lexical Environment
:
As we can see, every Execution context
has its own Lexical Environment
, which references the outer Lexical Environment
. In this case, it’s the Global scope, and If a function is nested inside another function, it will have a reference to the Lexical Environment
of the outer function, which in turn, references the Global scope. This is known as lexical scoping in JavaScript.
A closure is a combination of a function and the Lexical Environment
(scope) in which it was declared. It allows a function to retain access to variables from its outer scope even after the outer function has finished executing.
In simpler terms, when a function is called and popped off from the Call Stack, it checks if there is something referencing one or some of its local variables. It saves those variables in a special box called Closure.
function a() {
let name = "Nadjem"; /* this get accessed by the b() function
event after a() get called first */
function b() {
let hey = "Hello";
console.log(hey + " " + name) // "Hello Nadjem"
}
b(); // call b() second
}
a(); // call a() first
When I debug this code in the Chrome browser debugger, we can see more details; you can notice that the value of the name variable is preserved as a closure because it is referenced for the Lexical Environment
of the b() function, and if it’s not, it will be garbage collected or deleted after we call the function a().
In conclusion, comprehending the concepts of higher-order functions, execution context, and lexical environment is key to understanding closures in JavaScript.
Closures provide a powerful mechanism for preserving data and behavior by allowing functions to retain access to variables from their outer scopes.
By grasping these fundamental concepts, you can leverage closures effectively and unlock advanced programming techniques in JavaScript.
Also published here