How JavaScript works under the hood
Jul 16, 2022
22 min read
Many developers have been coding in JavaScript to carry out its legacy since Brendan Eich developed JavaScript back in 1997. If someone actually wants to learn JavaScript you need to understand JavaScript. You need to know the history of JavaScript, what problems it actually solves, and how JavaScript works behind the scenes. This background info will help you out when you will be coding and when you are learning some new features.
In this article, we will learn a little history of JavaScript, and how JavaScript works behind the scenes? What is the Runtime environment? What role does the JavaScript engine play in code execution? What the heck is Execution context? What is Scope, TDZ, and a lot more?
You ready 😉
JavaScript History
First of let's discuss the need for JavaScript, so you can have some idea of why are you using JS and what was its purpose.
In the early days of the web, there were ways to create web pages(HTML was released in 1993), but there was no effective way to manipulate them. As we can these days with DOM (Document Object Model - will be discussed later). So those static pages were usually non-interactive. This was the major reason some languages needed to be developed to give them life.
Back in 1995, Brendan Eich stepped forward and developed the first version of JavaScript in a mere 10 Days, it's not a typo it really was 10 Days. It was then called LiveScript(cool name, but I like JavaScript more). At that time JavaScript was like alien technology and was adopted quickly. The community kept expanding, it got better and better, and of course, the JavaScript we use is a monster compared to those days.
At first, It was developed for Netscape 2 and became the ECMA-262 standard in 1997. In 1997, ECMAScript 1 was released and was supported in Internet Explorer 4 for the first time. Then in 1998, ECMAScript 2 came out, and ECMAScript 3 in 1999. This fourth edition of the language was a bit delayed and was released in 2008 but could not make it to market. The fifth major edition, ECMAScript 5 was released in 2009. This version was used for a while and the latest version ECMAScript 6 was released back in 2015, which is now widely supported in all major browsers with exception the of Internet Explorer. here you can learn more about the history of your favorite language.
Here ECMAScript is
ECMAScript is a JavaScript standard meant to ensure the interoperability of web pages across different web browsers. It is standardized by Ecma International according to the document ECMA-262. Bascially it standardizes what JavaScript code dos what, so JavaScript behaves the same way in every Browser.
You cannot possibly learn the language if you do not know how that Language works under the hood. Let's first understand a few important concepts we will be using a lot in this article.
JavaScript in plain English
- JavaScript is an interpreted language, which means, like Java(no similarity with JavaScript except name ), it does not require us to compile it before sending it to Browser to be executed, the interpreter can take raw JS source code and execute it for us.
- JavaScript is single-threaded and synchronous in nature. This means it creates and executes all the tasks with a single execution thread. Tasks are queued one after another and the next task needs to sit and wait until the active task is completed. There are still ways to run JS code asynchronously, which we will discuss further in this article.
- JavaScript is a non-blocking programming or scripting language that does not block upcoming tasks if a program is taking too long, with the help of Web APIs, Callback queue, and event loop. For now, you might find this non-blocking statement contradictory to the above single-threaded statement. But we will discuss all these concepts in great detail in a while. You just need to be with me for the next few minutes.
- JavaScript is a dynamically-typed language, which means
var
can store any data type, like int, string, or object, unlike other statically typed languages like C, and C++ in which we have to explicitly specify variable typedatatype variable_name
.
The Runtime Environment
The Runtime Environment is a special environment that provides our code access to built-in libraries, APIs, and objects to our program so it can interact with the outside world and get executed to fulfill its noble purpose.
Runtime Environment is basically a factory where raw JavaScript code gets loaded and with a mixture of Web APIs, libraries, and objects it gets executed. Hopefully, this gives you some sense of the runtime environment. If you still have some doubts, do not worry we will be discussing components of the runtime environment and their part in JS execution in a short while. You just need to hang tight with me, and you will have a very clear picture of JS at the end of this guide.
In the context of a web browser, the runtime environment consists of the following components:
- JavaScript Engine
- Web Apis (like fetch, setTimeout, DOM, File API)
- The Callback queue
- The Event Loop
Runtime environment depends on the context, in which you are running JavaScript code. The runtime environment might look a bit different depending on the context. For instance in NodeJs environment JavaScript code has no access to Web APIs because they are a feature of web browsers. Basic features will be the same or similar though in case of any context.
A modern browser is a very complicated piece of software with tens of millions of line lines of code. So it is split into many modules that handle different logic.
Working of Web Browser and role of JavaScript Engine
Two of the most important parts of a web browser are the JavaScript engine and the rendering engine.
Rendering Engine
The rendering engine paints content on the screen. Blink is a rendering engine that is responsible for the entire rendering pipeline including DOM trees, styles, events, and V8 integration. It parses the DOM trees, resolves styles, and determines the visual geometry of elements on the screen.
JavaScript Engine
The purpose of the JavaScript engine is to translate source code that developers write into machine code (binary) that the processor can understand. So this is the place or factory you can say, where raw JavaScript gets downloaded, parsed, and transpiled into Machine code that can be understood by Processor.
JavaScript engine compiles and executes raw JavaScript source code into native machine code. So JavaScript engine is the place where JS source code is downloaded, parsed, interpreted, and executed at the end in the form of Binary code. Every major Brower vendor has developed its own JavaScript engine, which works the same way. Like Chrome has V8, Safari has JavaScriptCore, and Firefox uses SpiderMonkey.
We will be focusing on the V8 engine in this article. Read what is V8 Engine from Google's words;
V8 is Google's open-source high-performance JavaScript and WebAssembly engine, written in C++. It is used in Chrome and in Node.js, among others. It implements ECMAScript and WebAssembly, and runs on Windows 7 or later, macOS 10.12+, and Linux systems that use x64, IA-32, ARM, or MIPS processors. V8 can run standalone or can be embedded into any C++ application.
Working of the JavaScript Engine(V8)
The first version of V8 was released and used by the Google Chrome team in 2010 when they had some problems displaying Google Maps. Later on, they improved it over time and released another version of V8 in 2017, which is being used by the Chrome browser these days.
The current version was built on this model;
Let's break it down;
- V8 engine downloads JS source code and passes it to Baseline Compiler which processes and compiles code to lightweight ByteCode.
- This bytecode gets passed to an Interpreter which is basically an IntelliSense algorithm that knows what JS code does what. It interprets the bytecode to CPU-understandable binary code.
- At the very same time, Bytecode gets passed to the Optimization compiler which optimizes this ByteCode in the background while the application is running. It produces a very optimized binary code, which is eventually replaced with the code in the application, thus giving massive performance and boost.
This is the final model of the V8 engine. But this was not always like this. After many updates and remodeling, this very optimized version of the engine came into existence. Other browser vendors use different JavaScript engines. But they function pretty much in a similar way.
How JavaScript code works in the runtime environment
Unlike other programming languages, JavaScript is single-threaded in runtime, in plain English it means it can run only one piece (statement, line) of code at a time. Code runs sequentially, so when one process is taking longer to run, it blocks the rest of the code after waiting to be executed.
Consider this eternal loop;
Here is what happens when this program is opened in the browser, it uses a single JavaScript execution thread, which is responsible for handling everything on the web page, like scrolling, events handling, and fetching data from the server. Hence you see a Page Unresponsive prompt like this;
Thanks to modern browsers they use separate JavaScript execution threads for different tabs, otherwise, our browser would have been frozen. Modern web browsers usually use separate execution threads for different tabs or a single execution thread for one domain (same website on multiple tabs). Chrome uses a one-process-per-site policy so, if multiple tabs of the same domain were open, all of them would stop working. Other domains (websites) tabs will keep working fine.
Now, let's discuss the JavaScript engine in detail. JavaScript engine consists of mainly two components; The Callstack and the Heap.
The Heap
The heap is the unstructured memory storage where variables and objects get stored during program execution. Then the heap, also known as the "memory heap" gets cleaned during garbage collection. I will not talk about it in detail, you can check out this awesome article.
The Callstack or Execution Stack
We are interested in the call stack, which is a LIFO (first in, last out) data storage where current executing contexts are stored while the program is running. Meaning, latest function invocation will be added to the top of the so-called Execution stack, and executed before everything else. Each entry in the call stack is called a Stack frame. A stack frame contains information about the Execution Context, like its argument object, return address, local variables, etc.
What is an Execution context?
JavaScript engine creates a special environment for the execution of JavaScript code, which contains the code that is currently running and everything that aids in its execution. This special environment is called the Execution context.
During the Execution context runtime, specific code gets parsed by a parser, variables, and declarations get stored in memory, and executable bytecode gets generated. Which is then converted to binary code that gets executed.
Two kinds of execution contexts are created during runtime;
- Global Execution Context(GEC)
- Functional Execution Context(FEC)
Global Execution Context(GEC)
When the JS engine receives some script file, a default Global execution context is created to handle the code at the root of that file, everything that is outside of any function.
This is the main/default execution context, that encapsulates all of the functional execution contexts.
There is only one GEC for any script file.
Functional Execution Context(FEC)
When the Global execution context encounters a function invocation, a different kind of execution context, very specific to that function gets created which handles all the logic inside that function.
As there are usually multiple functions inside any script (JavaScript file), every function gets executed in its very own execution context, and there can be multiple Functional Execution Contexts in a single program.
Execution Context: Creation Phase
The creation phase of any execution context is completed in three main steps.
- Creation of Variable Object
- Creation of the scope chain
- Assignment of
this
value
Creation Phase: Creation of Variable Object(VO)
For GEC a variable object is created which is a memory container, which stores properties for all variables and function declarations and stores references to them. When a variable is encountered in a global execution context property is added to the Variable Object and is initialized(in case defined with var
) with the default value undefined
.
When a function declaration is encountered, a property is added to the Variable Object, and a reference to that function is stored as a value.
This means before even the start of execution of code, variables and function declarations are available to use. This concept in JavaScript is called Hoisting.
In the case of, FEC (functional execution context) doesn't create a VO but an array-like object called the argument object. All the arguments received by a function are stored in this array-like object.
Hoisting in JavaScript
JavaScript moves all the variables, function declarations, and class declarations to the top of their scope. This process is called hoisting in JavaScript.
In plain English, Before the execution of code, JavaScript stores all the variables, functions, and class declarations in the memory container, called Variable Object at the top of the scope, this process is known as Hoisting in JavaScript. Hoisting is the reason we can access functions and variables even before they are declared.
Function hoisting
Some JavaScript developers choose to define all of their functions at the top of the script and later call them at the end. The code below will still work fine due to hoisting, although we are calling the function before it was declared. Because function declarations are moved to the top of scope.
Variable Hoisting
Variables declarations are also hoisted.
Variables declared with the var keyword are hoisted and initialized with a default value undefined. Mean if you will try to access a variable declared with var
in advance, you will not get an error but the value will be undefined
.
Trying to access a variable declared with let
and const
keyboards before they are declared will result in a ReferenceError
. Some prefer to see let
, const
, and class
as non-hoisting, because the temporal dead zone (TDZ). strictly forbids any use of the variable before its declaration. This dissent is fine, since hoisting is not a universally-agreed term.
1console.log(v1); // undefined
2console.log(v2); // ReferenceError: Cannot access 'v2' before initialization
3console.log(v3); // ReferenceError: Cannot access 'v3' before initialization
4
5var v1 = "data 1";
6let v2 = "data 2";
7const v3 = "data 3";
8
9console.log(v1); // data 1
10console.log(v2); // data 2
11console.log(v3); // data 3
In case of let, const and class declerations (lexical declerations), starting from the top of the scope, until the complete initialization of a variable, that variable is said to be in a Temporal Dead Zone(TDZ). You should always try to access variables outside of its TDZ. If you try to access the variable from inside its TDZ, you will get ReferenceError.
Quiz Time: What would be the output of the code given below;
I am sure you guessed it right. The error will be thrown in this case stating something like myFunc is not a function, because at this stage value of the myFunc
variable will be undefined
due to Hoisting. So calling undefined()
will throw an error.
Creation Phase: Creation of Scope chain
The scope is a mechanism in JavaScript that determines which piece of code is accessible from where. When a variable, function, or class is declared in a script, it has some address and some boundaries. Beyond those boundaries, it is inaccessible. The scope does answer many questions like from where code can be accessed. and from where it can not be?
First of all, when the script is loaded in the engine, a global scope is created, which holds everything that is not inside any function. All the functions and their inner functions can access this global scope.
When a function is defined in another function, the inner function has access to the code defined in that of the outer function, and that of its parents. This behavior is called lexical scoping.
Whenever a variable or function is called somewhere, the engine starts looking for inside the local scope, where it was called. If not found, it looks for parent scopes (Lexical Scoping) one by one, from inner(local) to outermost (global scope). If the variable was not found in local and all the parent scopes including the global root scope, then the engine throws an error.
Every function execution context creates its scope which determines what variables and functions are accessible where. There are a few cases that we need to discuss to completely understand scoping.
Case 1: Variables declared inside a function can be accessed from anywhere inside that function, except for TDZ.
Case 2: Function declared inside a function has access to everything of its parent function and parent's parent function up to the global scope. This is called Lexical Scoping. This concept gave rise to something called closures in JavaScript.
When a function inside another function is called outside of its context, means outside of the parent function in which it was declared but it still has access to variables of the parent function even after the parent function has finished execution, this associative phenomenon is called closures. This is a very important concept and you definitely should check it out here.
Parent function has no access to anything declared inside of its child functions. The scope is like a one-way mirror, which means you can look outside but now one can look inside.
[Source: article]
As you can see in this image, the function second
has access to all scopes including its own local
scope, the scope of function first
, and the global
scope. But function first has only access to its local scope and the global scope. You can see it has no access to the scope of function second
.
The second step of the creation of the Execution context completes here.
Till now Variable object has been created, scope chain is in place. Let's discuss the third and final step.
Creation phase: Setting the value of this
The next and final step in the creation of execution context is setting the value of this
. this is a special keyword in JavaScript which refers to the scope of the environment where the Execution context belongs.
Value of this
is different, depending on the context it is being used. Have a look at these different cases;
Case 1: In Global Execution Context, this
refers to the global window object in the case of browsers. Try logging "this" to the console inside GEC and you will see a window
object. Here is one interesting thing to notice. When you define a variable or function inside GEC, they are saved as a property of the window object.
1var occupation = "full-stack developer";
2
3function addOne(x) {
4 return x + 1;
5}
6
7console.log(occupation); // full-stack developer
8console.log(window.occupation); // full-stack developer
9console.log(this.occupation); // full-stack developer
10console.log(this.occupation === window.occupation); // true
Case 2: Inside FEC, a new this
object is not created, but instead, it refers to the context it belongs to. Like, in this case, this
refers to the global window object. As this function is declared in GEC, this would refer to the global window
object.
Case 3: In the case of objects, using this
inside the methods does not refer to the global object, but the object itself. Look at this example;
Case 4: Inside constructor function, this
refers to the newly created object, when called with new
keyword like this;
With this, the creation phase of Execution Context has been completed. Till now, everything has been stored in VO, the scope chain is created and the value of this is in place.
Execution Context: Execution Phase
Right after the creation of the execution context. JavaScript engine starts the execution of created context. But Variable Object currently contains all the declarations of that specific execution context but their value is undefined. And you already know we cannot work with undefined
. So JavaScript engine again looks through VO once again and feeds their original values. After this code gets parsed by the parser engine, gets transpiled into lightweight bytecode, which is then converted to binary code(01), which then gets executed.
JavaScript Callstack(in terms of execution context)
First of all, the script is loaded in the browser, JavaScript engine creates the Global Execution Context. This GEC is responsible for handling all the code that is not inside of any function.
Initially, this GEC is the active execution context. When some function is encountered in this GEC, and new Functional Execution Context is created for the execution of that function. That FEC holds all the information about the execution of that function and everything that aids in its execution. This newly created execution context is placed right above the GEC. This process is repeated for every function call and FEC is added and piled up in the so-called Callstack.
Execution context on the top is executed first, and once that context is executed (something returned), it is popped out of the stack. The very next context becomes an active one and its execution starts. This process is repeated until there is the last GEC left in the stack. It is executed at the end and the script is said to be executed at this point.
Conclusion
Let's sum up all of this by an example. We try to recall everything we have learned so far in this article.
Consider this program inside the script file;
1var name = "Azhar";
2
3function first() {
4 var a = "Hi";
5 second();
6 console.log(a + " " + name);
7}
8
9function second() {
10 var b = "Hey";
11 third();
12 console.log(b + " " + name);
13}
14
15function third() {
16 var c = "Hello";
17 console.log(c + " " + name);
18}
19
20first();
21
22// Logs:
23// Hello Azhar
24// Hey Azhar
25// Hi Azhar
First of all this .js
file gets loaded in the browser, and passed to the JavaScript engine for execution. The engine creates a Global Execution Context for this file which handles the execution of the root of this script file, everything that is not inside a function. This GEC is placed at the top of the Execution Stack
. Global Execution Context is created during two phases; the creation phase, and the execution phase.
Variable name="Victor"
is stored in the Variable Object (VO) of GEC and initialized with the default value undefined
(hoisting).
Then for all these functions first
, second
and third
, a property is added to VO of GEC, and reference to these functions is stored as value.
After setting the VO of GEC, the scope chain is created, and value of this
set (window
).
Now starts execution. But the value of name
variable is still undefined
in the VO. And we cannot work with undefined, Right? So once again JS engine looks through VO and feeds the original value of name
variable. Now we are good to proceed further in our program.
First of all, the function first
gets invoked. JS engine creates a Functional Execution Context to handle its execution. This FEC is placed on top of the Global Execution Context, forming a Callstack or Execution Stack. For the period of time, this FEC is the active one, as we already know Execution Context on the top in Callstack is active. Variable a = "Hi!"
gets stored in the FEC, not GEC.
In the next statement, function first
invokes function second
. Another FEC is created and placed on top of the FEC of function first
. Now this FEC is active. Variable b="Hey!"
gets stored in the FEC.
Then function second
invokes function third
, similarly, FEC is created, and placed on the top of the Execution stack. Variable c = "Hello!"
gets stored in the FEC.
So far Execution Stack looks like this;
And logs Hello Victor
to the console. But wait! Where does Victor come from? Variable name
is not defined in the function third
. You guessed it right, Scope Chain. The function third
looks for name variable inside its local scope, but did not find it. Because of something called Lexical Scoping, it also has access to its parent scope, the Global scope. JavaScript engine looks for name
in global scope and finds it.
When function third
has completed all of its purposes, its FEC gets popped out of the Execution stack(Callstack).
Then the very first FEC below it becomes active context and starts execution. Logs Hey! Victor
to the console' and pops out of the Execution stack. Now the last FEC of this program becomes active, logs Hi! Victor
to the console. After execution of all of the statements, it is destroyed and pops out of the Callstack.
We are again left with only GEC in the Execution stack. As well it has nothing left to execute it also pops out of the stack.
Hopefully, this example cleared up most of your doubts. We have discussed the first component of the JavaScript Runtime Environment so far. Hopefully, you find it helpful.
So far we have learned
- A little history of JavaScript
- JavaScript Runtime, which is basically a special environment provided by the browser or context we run our code in. This environment provides us with objects, APIs, and other components so our code can interact with the outside world and get executed.
- Components of runtime environment; like JavaScript engine, Web APIs, Callback queue, and Eventloop.
- JavaScript engine, which again consists of Callstack or Execution Stack and the Heap.
- Execution Context, which is basically a special environment for the execution of JavaScript code, contains the code that is currently running and everything that aids in its execution. This special environment is called the Execution context.
- Types of execution context; like Global Execution Context(GEC) and Functional Execution Context(FEC). The creation phase of Execution Context, which completes in three phases; Creation of VO, Scope chain building, and the setting value of this.
- The creation phase of Execution Context, which completes in three phases; Creation of VO, Scope chain building, and the setting value of this.
- The execution phase of Execution Context, we learned how GEC is created once the script is loaded and how every function creates its own FEC. They keep stacking on one another unless they return something and get popped out of Stack.
- Scoping and Temporal Dead Zone, we learned how functions can access declarations from their parent scope via Lexical Scoping. We briefly discussed TDZ as well.
The drawback of JS single-threaded nature
As we all know JavaScript is single-threaded in nature, as it has only one heap and one stack. The next program has to sit and wait until the current program finishes execution, heap, and Callstack get cleared and the new program starts executing.
But what if, the currently executing task is taking so long, what if our current execution context is requesting some data from the server(slow server), definitely it would take some time. In this case, the Callstack queue will be stuck as JavaScript only executes one task at a time. All the execution contexts that are next to be executed will keep waiting until our current guilty Execution context is resolved. How we can handle this sort of behavior. How can we schedule some tasks, or park some expensive tasks so that we can keep our application going? How can we make our synchronous JavaScript asynchronous?
This is where Web APIs, Callback queue, and Eventloop come to the rescue. My plan was to discuss all of these concepts in a single guide, but the length grew more than I expected.
What Next?
Now we are left with the other three components of JavaScript Runtime,
- Web APIs
- The Callback Queue
- The Eventloop
I will be dropping another guide like this on these left components. If you want to get notified of the next part, follow me 😉.
This was the first component of the JavaScript runtime, the Working of the JavaScript engine in the code execution. I have tried to simplify all the concepts so even absolute beginners can understand them. Hopefully, you were able to understand it, and found it helpful.
Credits and Motivations
While writing this guide, I found some helpful articles you should also check. These articles helped me write this extensive guide.
- Great article How does JavaScript and JavaScript engine work in the browser and node? by Uday Hiwarale.
- An extensive article on JavaScript Execution Context by Victor Ikechukwu.
- Guide on JavaScript Runtime Environment by Gemma Croad.
Final Words
If you liked this guide and want to get notified of my next articles like this, please do follow me. If your friend is struggling with these concepts, please do share this guide.
See you soon💓