In our previous interaction, we understood how Javascript is a Just In Time Compiled language. It's time to understand the internal workings of the Javascript Runtime Environment. There are two major types of Javascript Runtime Environments i.e. The Browser Runtime Environment and the Node Runtime Environment. In this article, we'd be studying the former one.
The Browser Runtime Environment
The following diagram highlights the major components of the Browser Runtime Environment.
JS Engine:
The Javascript Engine consists of the Heap Memory and the Call Stack. The Heap Memory can be considered to be a large memory pool used for storing non-primitive objects (i.e. Everything, except the seven primitive JS datatypes which are, integer, string, boolean, string, null, undefined, symbol). Whereas the Call Stack is where the so-called 'Execution Contexts' are stored, and function execution happens. We will be discussing this in greater detail later.
Web APIs:
Contrary to popular belief, common functions like document.getElementById(#some_id); document.querySelector('.some-class');
are not a part of Javascript, but indeed a part of Web APIs. Hence these functions are not readily accessible on the Node Runtime Environment. Make sure to bookmark this handy MDN Link for a list of Web APIs.
Callback Queue:
The Callback Queue contains a list of functions that will be executed when the call stack becomes empty. The Callback Queue functions like a classical queue in a First In First Out manner, whereby the event loop keeps checking if the call stack is empty, in which case it pops the front of the Callback Queue and puts it into the Call Stack, and the execution of the function happens. For a better understanding, consider the following code:
setTimeout(() => { console.log("Now You know Javascript"); //Output 1 }, 2000); console.log("Start learning Javascript"); //Output 2
What happens when you paste the code into the browser console? You'd observe that Output #2 gets printed before Output #1. At this moment stop and ask yourselves two questions: Why? and How?
The 'Why' can be intuitively guessed by looking at the setTimeout() function. It mentions '2000' in there, so quite possibly it's delaying the execution by 2000 milliseconds you'd guess and that's a very accurate guess.But the 'How' isn't so simple. Does the Call Stack have some mechanism to delay the execution of the function after 2 seconds, you'd wonder? Unfortunately, our Call Stack isn't such a patient entity. It's quite the sincere worker you see and it doesn't like to sit on tasks assigned to it. Au contraire, it will try to get any method/function on its plate as soon as it can do so. So some mechanism must exist to delay the access of this method to the Call Stack.
This is where the Callback Queue and Event Loop come into the picture. The Callback Queue will accept the code inside the timeout function only after the expiry of the specified time period. There the function will wait for its turn to reach the front of the queue, and the event loop will check if our diligent call stack is empty, in which case the function gets pushed into the stack and gets executed. Meanwhile, the code corresponding to Output 2 has already been executed, and the Output is visible on your console.
The Call Stack and The Execution Context
Our industrious worker Call Stack deserves a closer look into its activities. As mentioned above the call stack stores the so-called 'Execution Contexts'. These execution contexts can be thought of as isolated rooms where the execution of individual pieces of javascript code happens. There are two kinds of execution contexts:
Global Execution Context: Each javascript file can only have one Global Execution Context. This is where all the code that's not written inside a function gets stored.
Function Execution Context: Each function has its function execution context.
Ok let's take a pause, as I can hear Linus Torvald shouting at me
Talk is cheap. Show me the code
const learnerName = 'New Learner';
const first= () =>{
let a = 5;
const b = second(); //Line B
a = a + b;
return a; //Line D
}
function second () {
let c = 2;
return c; //Line C
}
const x = first(); //Line A
The Execution Contexts created here will be as follows:
Global Execution Context {
learnerName = New Learner
first = <function>
second = <function>
x = undefined
}
First Execution Context {
a = 5
b = undefined
}
Second Execution Context {
c = 2
}
As can be intuitively inferred, each Execution Context typically consists of:
Variable Environment:
Consisting of let, const and var
Functions
Arguments Object
Scope Chain
This Keyword
However, there's a major exception to this rule i.e. the Arrow Functions, whose execution contexts Do Not contain the Argument Object and the This Keyword. Make a note of this point, as 'this' is going to be very important in subsequent classes.
The above code gets executed in the following steps:
Global Execution Context is created with the values mentioned above
With the first function call happening at Line A. A Function Execution Context for First Function gets created.
Inside the First Function Execution Context, a Second Function Call happens at Line B and the Second Function Execution Context Gets Created.
Finally, the Second Function Execution Context returns a value of 2, from Line C. The Second Function Execution Context gets removed from the Call Stack and the control moves back to Line B, where 'b' is assigned a value of 2.
We're still inside the First Function Execution Context, b is added to a, and the new value of a is returned to Line A. The First function Execution context is cleared from the Call Stack
All this for such a simple piece of code. Make sure you take your time to internalize this bit of function calls, as this forms the basis upon which you'd draw call stacks for much more complex recursive function calls.
Conclusion
This has been a detailed discussion on the call stack, and I assume you enjoyed reading it as much as I enjoyed writing it. See you in the next discussion, where we'll be learning about Scope Chain and Hoisting.
Chapter 4