DEV Community

Cover image for How JavaScript Works?
Archit Sharma
Archit Sharma

Posted on • Edited on

How JavaScript Works?

In this article we are going to take a look at the computer science behind Javascript.

NOTE: Keep in mind that you do not need to understand these concepts to begin using JavaScript productively. Years of development experience may be required before they truly sink in.

We learnt in the last article that Javascript is a programming language based on the ECMA 262 Spec, but to study how it works in a computer system, we must travel to the absolute bottom of the stack, which is the bare metal CPU and Memory on a machine.
Level of Abstraction

When you run a JavaScript programme, whether it's a web application in the browser or something server side with nodejs, it needs to allocate memory on your RAM to store things for the runtime and variables and objects that you reference in your code, and it also needs a thread from your CPU to actually execute the instructions in your code, but here's the thing: as a JavaScript developer, you never really have to think about this stuff because it's a high-level language.

High-Level: refers to the abstraction the language provides over the machine’s bare-metal hardware. JavaScript is considered high-level because it does not require direct interaction with the operating system, hardware. In addition, it does not require memory-management like C/C++ because the runtime always uses garbage-collection.

But what exactly do we mean by high-level? We're referring to the degree of abstraction or simplification that the language provides over the computer's hardware. Machine code is the lowest level language; it is a numeric language that can be executed directly by the CPU; however, it would be extremely difficult to build a website with it because you would have to memorize a number for each and every instruction that you want to run.

Moving up one level to Assembly provides some syntactic sugar, but each assembly language is specific to a specific CPU or operating system
So we can move up another level to the C language, which provides a modern syntax and the ability to write cross-platform programmes, but developers must still be concerned with low-level issues such as memory allocation.

Another step up brings us to languages like JavaScript and Python, which use abstractions like garbage collectors and dynamic typing to simplify the way developers write their applications.
Working of Interpreter and Compiler

There are two basic techniques to convert programming language code into something that the CPU can actually execute, so let's go ahead and define a few crucial terminology related to JavaScript.
One of them is called an Interpreter, and the other is called a Compiler.

Interpreted or Just-in-Time Compiled: Interpreted means the source code is converted to bytecode and executed at runtime (as opposed to being compiled to a machine code binary at build time). This is also why JS is commonly called a “scripting language”. Originally, it was only interpreted, but modern JS engines like V8, Spidermonkey, and Nitro use various techniques to perform Just-in-Time Compilation or JIT for better performance. Developers still use JS like an interpreted language, while the engine magically compiles parts of source code to low-level machine code behind the scenes.

JavaScript is an interpreted language, which means that it requires an environment to read the actual source code and be executed. We can show this by simply opening the browser and running some JavaScript code from the console.
This is different from compiled languages like Java or C, which will statically analyse all of your code beforehand and then compile it into a binary that you can actually run on the computer.

JavaScript is a dynamically typed language, which tends to be a common characteristic with high-level interpreted languages.
We can examine this by comparing some statically typed Java code to some dynamically typed JavaScript code.

Dynamic Weakly Typed: Dynamic most often refers to the type system. JS is dynamic weakly typed language, meaning you do not annotate variables with types (string, int, etc) and the true types are not known until runtime.

Static vs. Dynamic typed languages

When comparing, you'll notice that the Java code annotates things like integers and strings, but the JavaScript types are unknown or implicit. This is because the type is associated with a runtime value rather than the actual variables or functions in your code.

You may also hear that JavaScript is a Multi-Paradigm language at this point. Most general-purpose programming languages support multiple paradigms, allowing you to combine declarative functional techniques and imperative object-oriented approaches.

Multi-Paradigm: means the language is general-purpose or flexible. JS can be used for declarative (functional) or imperative (object-oriented) programming styles.

Prototypal Inheritance

Prototypal inheritance is the foundation of JavaScript. Everything in JavaScript is considered to be an object, and each object in the language has a link to its prototype, forming a prototype chain from which subsequent objects can inherit their behaviours. If you're used to class-based inheritance, this could seem strange to you, but it's one of the low-level ideas that makes JavaScript a very flexible multi-paradigm language.

Prototypal Inheritance: means that objects can inherit behaviors from other objects. This differs from classical inheritance where you define a class or blueprint for each object and instantiate it. We will take a deep dive into prototypal inheritance later in this course.

Here is an Example:
let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal; // (*)

// we can find both properties in rabbit now:
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
Enter fullscreen mode Exit fullscreen mode

Javascript is a High-level Interpreted, Dynamically Typed, Multi-Paradigm Prototype Language, but it is also a Single Threaded, Garbage Collected, Non-Blocking language with an Event Loop that can be compiled Just-In-Time.
The first set of definitions is largely relevant to how javascript is laid out in ECMA 262 - however it doesn't describe how memory should be managed and it doesn't even mention the event loop in the entire 800 page document, so it's up to browser vendors to handle these implementation details.
JIT Compilatio

Spider monkey from Mozilla and v8 from Google are two of the most popular implementations. Their methods differ slightly, but they both use a technique known as Just-In-Time Compilation.

In the case of v8, it will compile all of your JavaScript down to native machine code before running it, rather than interpreting bytecode line by line as a normal interpreter would.

So, while these JavaScript engines do not fundamentally change the way developers write code, the JIT compiler improves performance in browsers and on Node.

Because JavaScript is a single-threaded language, it can only do one computation at a time.

Try this : Open the console with Ctrl+Shift+J in this browser tab and then build a while loop that never stops.
while(true){}
Enter fullscreen mode Exit fullscreen mode

If you try to click on something in this browser tab, it will never catch the event since the single thread is locked in that while loop and can't move on to the next event.

Task Manager ss
Enter Chrome Task Manager and you should see the browser tab using nearly 100% of the CPU cores resources. Go ahead and end the process and refresh the tab.

When you run your JavaScript code, the machine allocates two regions of memory: the call Stack and the Heap.

The call Stack is intended to be a high-performance continuous memory region used to execute your functions. When you call a function, it generates a frame and a call stack with a copy of its local variables. When you call a function within a function, it adds another frame to the stack, but when you return from a function, it removes that frame from the stack.

I believe that going through some of your own code frame by frame is the best approach to grasp the call stack.

Here is a video for that:

When we encounter something a little more complex, such as an object that may be referenced by several function calls outside of this local context, the Heap comes into play.

The Heap is Garbage Collected, which means that V8 or the JS runtime will try to clear up free memory when it's no longer referenced in your code. This doesn't mean you shouldn't be concerned about memory, but it does mean you don't need to manually allocate and free up memory as you would in C.
Event Loop Stack heap

We've already seen how a simple while loop can completely break a single-threaded language, so how can we handle any kind of long-running task? The answer is the Event Loop.

Event-Loop Concurrency Model: Event Loop refers to a feature implemented by engines like V8 that allow JS to offload tasks to separate threads. Browser and Node APIs execute long-running tasks separately from the the main JS thread, then enqueue a callback function (which you define) to run on the main thread when the task is complete. This is why JS is called non-blocking because it should only ever wait for synchronous code from your JS functions. Think of the Event Loop as message queue between the single JS thread and the OS.

Let's start from scratch and write our own code. In its most basic form, it's just a while loop that waits for messages from a queue and then processes their synchronous instructions until completion.

while (queue.waitForMessage()) {
  queue.processNextMessage();
}
Enter fullscreen mode Exit fullscreen mode

In the browser, you're already doing this without even thinking about it; you might set up an Event Listener for a button click, and when the user clicks it, it sends a message to the queue, and the runtime will process whatever JavaScript you defined as the callback for that event; this is what makes JavaScript Non-Blocking.

Because the only thing it ever does is listen to events and handle callbacks, it's never actually waiting for a function's return value; instead, it's waiting for the CPU to process your synchronous code, which is usually on a scale of microseconds.

Consider the first iteration of the event loop. It will first handle all of the synchronous code in the script, and then it will check if there are any messages or callbacks in the queue ready to be processed. We can easily demonstrate this behaviour by adding a set timeout to the top of the script for 0 seconds.

Here is an example that demonstrates this concept (setTimeout does not run immediately after its timer expires):
const seconds = new Date().getSeconds();

setTimeout(function() {
  // prints out "2", meaning that the callback is not called immediately after 500 milliseconds.
  console.log(`Ran after ${new Date().getSeconds() - seconds} seconds`);
}, 500)

while (true) {
  if (new Date().getSeconds() - seconds >= 2) {
    console.log("Good, looped for 2 seconds")
    break;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now you might think that, this timeout should be executed first because it is at the start of the file and has a timeout of 0 seconds, but the event loop won't get to it until the first iteration of synchronous code is completed.

What makes this unique is that you can offload long-running jobs to completely separate thread pools in the browser. For example, you might make an HTTP call that takes a few seconds to resolve or interact with the file system on Node JS, but you can do so without blocking the main JavaScript thread.
Micro Task queue

With the introduction of Promises and the Micro Task Queue, JavaScript had to go and make things a little more strange.
If we include a Promise resolve after the setTimeout() in our script,

setTimeout(() => console.log('Do this first?'),0)
Promise.resolve().then(n => console.log('Do this Second?'))
Enter fullscreen mode Exit fullscreen mode

You'd think we have two asynchronous operations with zero delay here, so the set timeout would fire first and the promise second, but there's something called the Micro Task Queue for Promises which has priority over the main task queue used for Dom APIs and set timeouts, etc. This means that the handler for the Promise will be called back first. In this case, as the Event Loop iterates, it will first handle the synchronous code, then it will go to the Micro Task Queue and handle any callbacks that are ready from your promises.

It will finish by running the callbacks that are ready from your set timeouts or Dom APIs.

So this is how JavaScript works, If all of this seems overwhelming, don't worry because you don't need to know any of it to start building things with JavaScript.
Thank you for reading this article; do follow me for more.

Top comments (1)

Collapse
 
sahilali profile image
SAHIL ALI

Great!! Thanks for sharing