DEV Community

Cover image for 0 to 1- How JavaScript works under the hood
Azhar Dev
Azhar Dev

Posted on • Updated on

0 to 1- How JavaScript works under the hood

Many developers have been coding in JavaScript to carry out its legacy since Brendan Eich developed JavaScript back in 1995. If someone actually wants to learn JavaScript you need to understand JavaScript. You need to know the history of JavaScript, what problem it actually solves, and how JavaScript works behind the scene. 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 little history of JavaScript, 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 (yeah, stone Age 😉), 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 language needed to be developed to give them life.

Back in 1995, Brendan Eich stepped forward and developed the first version of JavaScript in mere 10 Days, it's not a typo I really meant 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, 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 and 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 non-blocking programming or scripting language which does not block upcoming tasks if a program is taking so 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 type datatype 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 full fill 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 following components:

  • JavaScript Engine
    • The Callstack or Execution Stack
    • The Heap
  • 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 upon 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 which handle different logic. 

Two of the most important parts of a web browser are the JavaScript engine and a 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 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 form of Binary code. Every major Brower vendor has developed its own JavaScript engine, which basically 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 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 Chrome browser these days.

The current version was built on this model;

V8 JavaScript engine model

[Image copied from this awesome article by Uday Hiwarale]

Let's break it down;

V8 engine downloads JS source code and passes it to Baseline Compiler which process and compiles code to lightweight ByteCode.

This bytecode gets passed to an interpreter which is basically an IntelliSense algorithm that knows what JS code dos 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.

This is the basic functioning model of Chrome JavaScript Engine(V8). Other browser vendors use different JavaScript engines. But they function pretty much a similar way.

How JavaScript code works in the runtime environment

Unlike other programming languages, JavaScript is single-threaded in runtime, in plain English means it can run only one piece 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 that waiting to be executed. Hence sometimes you might see, a Page Unresponsive alert. That usually occurs when some infinite loop of code is encountered in runtime. And it keeps running until all the CPU resources are consumed.

Consider this eternal loop;

Eternal while loop

Here is what happens when this program is opened in 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.

When this kind of infinite loop or huge JavaScript program is encountered, the execution of code gets blocked after it. Because JavaScript is single-threaded in nature and runs one piece at a time. Thus infinite loops keep running again and again until the system is out of resources. And we have to see the "Page Unresponsive" dialog.

Page unresponsive dialog

Thanks to modern browsers they use separate JavaScript execution threads for different tabs, otherwise, our browser would have been frozen if such a heavy program or infinite loop was encountered in one tab on one page. 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 domains were open in different tabs, all of them will stop working. Other domains (websites) tabs will keep working fine.

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 basically a LIFO (first in, last out) data storage where current executing contexts are stored while the program is running. We will discuss what Execution Context is in detail in a while. 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(VO), and executable bytecode gets generated which is then converted to binary code, which 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 which 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.

How Execution Context is created?

As we already know that execution context has two major tasks, it prepares the script and then executes it. So we will try to understand the execution context in two phases;

  • Creation Phase
  • Execution Phase

Creation Phase

The creation phase of any execution context completes 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 basically 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. Due to hoisting. We will discuss hoisting in great detail in a while.

In 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, Prior to 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. Basically 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.

Function hoisting

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, you will not get an error but the value will be undefined.

If you declare a variable with a let or const keyword, their declaration is hoisted but not initialized with a default undefined value. That's why you will get an uncaught ReferenceError, when we try to access it before the declaration.

Variable hoisting

Here one thing is really important to discuss. You might be asked in an interview about this.

Starting from the top of the scope, until the complete initialization of a variable, that variable is said to be in 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. Here the question arises of where TDZ starts, and where it ends.

See the code below, you will know Temporal Dead Zone starts at the start of the code block(top of scope) and ends at initialization. Variables declared with let and const follow the same pattern.

Temporal Dead Zone in case of let and const

In the case of variables declared with var the scenario is a bit different. And a guilty guy is hoisting. As we already know variables declared var are hoisted and initialized at the same time with a default value undefined. And we also know TDZ ends when a variable has been assigned a value. 

This is the reason var behaves a little differently compared to let and const. See this code;

Temporal Dead Zone in case of var

Hopefully, TDZ is all clear to you. Let's get back to hoisting.
There is one strict rule of hoisting, that hoisting only works for statements, not expressions. Have a look at this code;

Hoisting rule only statements

This is because we are assigning a function expression to a variable as a value. As we all know variables declared with let keyword are hoisted, but throw ReferenceError if called within Temporal Dead Zone. This is because their value is not initialized during hoisting and TDZ only ends after the complete initialization of the variable.

Quiz Time: Here is a small question for you? What would be the output if we used var instead of letting in the above code example. Can you guess whether any error will be thrown or function output will be logged to the console?

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 which 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, global scope is created, which basically holds everything which 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 an 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 Temporal Dead Zone.

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.

Scope illustration

[Image copied from this awesome article by Victor Ikechukwu at freeCodeCamp]

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 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. This means these 2 statements are equivalent;

this in GEC comparison

And this will log true to the console;

this in GEC comparison 2

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.

this in FEC

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;

this inside object methods

Case 4: Inside constructor functions, this refers to the newly created object, when called with new keyword like this;

this inside constructor functions

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 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

et'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;

Execution stack conclusion img 1

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 bottom(or 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. Hoisting you know 😉.
 
After setting the VO of GEC, the scope chain is created, and value is 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, which is Victor. Now we are good to proceed further in our program.

Execution Stack Visual

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.

Execution Stack Visual phase 2

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.

Execution Stack Visual phase 3

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;

Execution Stack Visual phase 4

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).

Execution Stack Visual phase 5

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.

Execution Stack Visual phase 8

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 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 really awesome articles you should also check. These articles helped me write this extensive guide.

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, do share this guide.

Till then, Stay safe, and try to keep others safe.

See you soon💓

Top comments (0)