JavaScript code is always executed in JavaScript runtime. It can be browsers or NodeJS or maybe something similar. In this post, let's understand how browsers enable JavaScript code execution.
Every browser has a code execution environment called JS runtime. JavaScript runtime is made up of components like the JavaScript engine, web APIs, callback queue and the event loop. These components are illustrated in the image shown below -
Let's understand each of these components in a little detail.
JavaScript engine is made up of call stack and heap memory. Callstack is where JavaScript code is executed and heap memory is where the objects in JavaScript are stored.
Web APIs are a collection of functionalities provided by the browser. This includes fetch API for making HTTP requests, timers like setTimout
and setInterval
, alert
, prompt
, intersection observer and many others.
A callback queue is where callback functions are registered when the JS code is parsed. All the callback functions are registered here and when they are called, they are transferred to the call stack where they are executed.
The event loop can be considered as a mechanism that puts together the callback queue and the call stack. So, whenever a callback function is called, the event loop puts it in the call stack and then it is executed in the call stack.
Besides these, there is one more concept called global execution context. This is where all the top-level code is executed. Top-level code is the code that is not present inside any callback function. Consider this example -
import styles from "../styles/home.css";
const title = "This is the title for home page";
const description = "This is description....";
const renderOutput = () => {
const isDemo = true;
if (isDemo) {
return title;
} else {
return description;
}
};
console.log(renderOutput());
The import statements, title and description variables, and the renderOutput function are a part of top-level code.
NOTE: Every JS program will have exactly one global execution context.
And every function has its own execution context. Execution context can be considered as an environment of a function that contains the details of that function, like arguments, local variables, function code itself and a few other things.
Ok, enough of the theory part. Let's put it all together and understand it with a code example. Consider the code shown below -
const num = 12;
const cube = (num) => {
const squareOfNum = square(num);
return squareOfNum * num;
};
function square(num) {
return num * num;
}
const answer = cube(num);
console.log(answer);
All the top-level code goes in the global execution context. So, the global execution context for this example will look something like this -
Now, each function has its own execution environment. The arguments that we pass in the function and the local variables declared inside the function come in this execution environment. These variables can be accessed from that function only and nowhere outside that function. But everything declared in the global execution context can be accessed inside any function environment. This is how scoping works in JavaScript. More on scoping in a separate blog.
Once these environments are created while parsing of code, then comes the execution of code. This happens in the call stack.
The call stack is simply the collection of execution contexts placed on top of each other. The context present on the top is always executed first and it will be removed as soon as it is completely executed. The next context will be executed only when the context above it has been executed completely.
For the code given above, the call stack and execution contexts will look something like this -
The global execution context is placed first in the call stack. And as soon as the cube function is called, its context is placed on top of GEC and it starts executing its code. This involves calling the square function, which places the context of the square function on top of the cube context. Now, the execution of cube function will pause temporarily and the square function will start executing.
The square function is executed completely and it is removed from top of the call stack and the execution of cube function resumes from the point where it was paused. Once its execution is complete, it will also be removed from the call stack.
The global execution context will also be removed from the call stack when the browser tab is closed and the program terminates.
Besides these things, there is a lot more that goes behind the scenes. But this article was only supposed to give you a brief overview of working of JavaScript in browsers.
Just a side-note, NodeJS also provides a JavaScript runtime but it is very different than the runtime provided by the browsers. NodeJS gives us access to file systems which browsers don't have and browsers give access to Web APIs like fetch, intersection observer and many others which are absent in NodeJS.
That's it for this blog. More beginner-friendly blogs coming soon 🙌