How does JavaScript Work?

How does JavaScript Work?

Introduction

Have you ever wondered why JavaScript is considered such a weird language? Why does it behave unexpectedly sometimes? How is it possible to call a function even before declaring it? What is a closure or hoisting? Why setTimeout() function with a timer ⏳ of say 3 seconds may or may not exactly run after 3 seconds? The answer to all these questions boils down to one thing i.e. How JavaScript works and how it is executed in the browsers? If you understand this one thing then many things in JavaScript start to make sense and you'll be able to catch bugs quickly and write better code. In this article, I'll help you learn how JavaScript works?

Let's take a look at this statement

JavaScript is a synchronous single-threaded language

This means in Javascript statements are executed one at a time and in a specific order. Let's look at the following code.

console.log("foo") 
console.log("bar")

In the above example first foo then bar is logged inside the console.

In order to understand how JS works, we need to familiarize ourselves a little bit with The Call Stack

The Call stack

According to the MDN docs

A call stack is a mechanism for an interpreter (like the JavaScript interpreter in a web browser) to keep track of its place in a script that calls multiple functions — what function is currently being run and what functions are called from within that function, etc

Do you get it? NO. Let's take an example.

Imagine you have lots of books and you want to read them all so you come up with a solution. You stack all the books on top of one another and give yourself the following constraints.

  • To read a book you must pick up the topmost book in other words the one which was added last in the stack.
  • You cannot read a book if there is already a book placed on top of that book. You must finish the topmost book, remove it from your stack and read the one below it and keep doing this until you have read all the books.
  • If you buy another book then you put that book on top of that book stack and start reading this new book immediately and when you finish this new book you can go back to the previous book and start reading where you left off.

This approach follows the Last In First Out (LIFO) principle i.e. the thing which was added last is removed first.

The call stack in javascript works very similarly to our book stack.

In order to manage the execution contexts, JS engines uses a call stack. This call stack is a data structure that keeps track of information about functions being executed.

  • When we invoke a function then the JS Engine adds an execution context to the call stack and starts carrying out the function.
  • If this function also invokes another function then a new execution context is created and added on top of the call stack.
  • When a function is finished executing then its execution context is removed from the call stack.
  • If the call stack takes more space than it had been assigned then we get a 'stack overflow' error.

The call stack has been dubbed many names such as Program Stack, Control Stack, Runtime Stack, Machine Stack.

The Execution Context

When we run a Javascript code a Global Execution Context is created and pushed into the call stack. It can be imagined as the box or a container where all variables and functions are stored as key-value pairs, and the code gets evaluated and executed.

execution-context.png

This global execution context has 2 phases or components

  1. Memory Creation Phase or Variable Environment
  2. Execution Phase or Thread of Execution

Memory Creation Phase

Whenever JS code is executed the global execution context (G.E.C.) goes into the memory creation phase. During this phase following things happen

  • a global object is created window in case of browsers, global in node.js
  • a global variable this is created which refers to the global object
  • all the variables are allocated memory and are initialized with undefined
  • in the case of functions the entire function is stored directly in memory.

Let's take an example

var a = "rishu"
function greet(name){
  console.log("Hello", name)
}
greet(a)

When we run this code a global execution context is created and initially, the code will go through a memory creation phase and memory is allocated to all the variables and functions. Here the a variable is allocated memory with an undefined value. The greet function is also allocated memory but instead of undefined, the entire function is stored in that memory space.

Initialization-of-a.png

greet-function-stroed-in-memory.png

Now the program goes into the execution phase

Execution Phase

In this phase, the code is executed line by line.

Let's go back to our example

var a = "rishu"
function greet(name){
  console.log("Hello", name)
}
greet(a)

In the above code as soon as the program encounters var a = "rishu" then variable a is assigned the value "rishu" which was initially assigned undefined

value-assign-to-var.png

Now control goes to the next line, from lines 2 to 4 there is nothing to execute, and our function was allocated memory in the previous phase. So control goes to the last line greet(name)

greet(name) is a function invocation so another execution context or a function execution context is created and pushed inside the call stack on top of the global execution context which was pushed earlier in the call stack. This execution context also goes through 2 phases mentioned above.

call-stacks.png

During the memory allocation phase of the function execution context following thing(s) happen

  • name is allocated memory and initialized by undefined

Now comes the execution Phase of function execution context

  • Value "rishu" is stored inside that variable name as it was passed during function invocation and control reaches to next line
  • Next line logs Hello rishu into the console

As soon as the function greet(name) is executed the function execution context is popped out from the call stack. Now the control goes back to the global execution context and since there is nothing more to be executed in our program this global execution context is also removed or popped out of the call stack and our Javascript program is completed executing.

Mystery Resolved

Now you can understand why we can invoke a function statement even before initializing it in our code. It is because when we run our code then the function statements get stored in the memory before the execution begins and if we invoke our function before its initialization it will be called as it is already in our memory space.

The same goes for our variable declaration because undefined is assigned to our variable during the memory creation phase so if we log a variable before its initialization then undefined is logged in the console.

Edge Case(s)

  • What if we use a function expression instead of a function statement i.e we declare our function like this
var greet = function (name) {
  console.log("Hello", name);
}

In this example, we are storing an anonymous function inside our greet variable so this will behave the same as a variable, and undefined will be assigned to greet during the memory creation phase. And the function will be assigned to greet in the execution phase.

greet-undefined-funct-expresion.png


Hoisting

JavaScript Hoisting refers to the process whereby the interpreter appears to move the declaration of functions, variables, or classes to the top of their scope, prior to execution of the code.

Now it is very easy to understand hoisting since we know that memory is allocated to variables and functions prior to execution so we can access them before their initialization and it seems like the interpreter has moved our declarations to the top. But in reality, all these declarations are hoisted because they have been allocated memory prior to execution during the memory allocation phase

Final Words

NOTE: You might have noticed that we have declared all variables using the var keyword and we are taking examples of function statements and not storing them in let or const. This is because let and const behave differently and they are not hoisted as well, and we still haven't got answers to the questions which arose in the beginning like Why the setTimeout() functions with a timer ⏳ of say 3 seconds may or may not exactly run after 3 seconds? We'll see that in upcoming articles in this Javascript series.

per aspera ad astra