Making JS less scary with knowledge of Execution context, call stack and hoisting

Making JS less scary with knowledge of Execution context, call stack and hoisting

Intro

JavaScript is hard.

Don’t get me wrong, JS is the go-to language to become a full-stack developer in the least time and most miniature tech stack. In the long run though, without the knowledge of the inner workings of JavaSript, your codebase starts to have bugs and you’re clueless about what introduced bugs in your project. An understanding of the inner workings of JS allows you to explore the language beyond the surface level.

In this blog, our prime focus will be on - Execution context, Call stack and Hoisting.


Execution Context

Think of an execution context as a container that keeps track of what variables and functions are available in a particular part of your code. It ensures that each part of your program knows what it can use and how to access it without causing conflicts with other parts of the code.

In more simpler terms, execution context contains details about your variables, functions and the scope chain.

Types of Execution Context

  1. Global Execution Context (GEC)

    Code that is written outside of JS functions in the file, resides in a global execution context. Global Execution Context is created only once for each JS file. This is also the default execution context. Usually, when JS execution starts in the browser, 3 things are created.

    1. Global Execution Context

    2. Window Object in case of browser runtime

    3. this keyword, that points to the window object

  2. Function Execution Context (FEC)

    A new environment is created each time a new function is invoked. This environment is similar to GEC but local to the function that is currently executing.

  3. Eval Function Execution Context

    Whenever eval() is called a new context is created. Using this function should be avoided at all costs as it evaluates the string that is passed in the parameters.


Creation of an execution context

There are 2 phases in which an execution context is created. They are :

  1. Creation phase

  2. Execution phase

1. Creation phase

The JS engine starts the compilation in this phase. It does not execute any functions or assign values to any variable. JS engine just registers the existence of your functions and variables here.

Three things mainly concern the creation phase. They are :

  1. Variable Object (VO) Creation -

    A variable object is created in the first step of the creation phase. All the variable declarations with the keyword var are initialized with undefined and stored in the Variable object.

    The function definitions are stored in the variable object along with all the parameters passed in the function. The functions are not executed here. This point is crucial for understanding hoisting.

    Let us look at the below snippet and examine the creation phase in the browser.

     var a = 10;
     var b = 20;
    
     function add(x,y){
         console.log( x + y )
     }
    
     add(a,b)
    

    As you see, on pausing with the debugger on the very first line of the code, a and b are already initialized and stored in the Global object. Even the function definition has been stored.

  2. Creation of the scope chain

    A scope chain is created for functions and closures in this step. JS checks for the current local scope inside a function to see if your variable is present there. If not, it goes over to the parent scope in the chain. This continues until the last link in the chain is reached i.e. The Global Scope. I’ve discussed more about it in this article.

  3. Determining the value of the this keyword

    The value of the this keyword differs based on how the function is invoked. This value is set right before the execution phase begins.

    In global scope, this refers to the window object.

     console.log(this); // Output: [object Window]
    

    In the case of methods from an object, this refers to the object itself.

    Lastly, in the case of constructor invocation using the new keyword, this refers to the newly created instance.

     function Song(name) 
      this.name = name;
     }
     const song = new Song("Castle of glass");
     console.log(song.name); // Output: "Castle of glass"
    

To sum it up, the creation phase has a total of three steps. Moving on to the execution phase.

2. Execution Phase

JS engine checks the Variable Object one more time and assigns value to the variables. It starts the execution of your functions during this phase.

Now that we’re aware of the execution context, it's finally time you ask, “All this is okay, but what is the role of the Call Stack?”


Deciphering the Call Stack: How JavaScript Manages Function Calls

The call stack manages the execution context and function calls.

Call stack records where in the code we currently are. Since it’s a stack data structure, it uses the Last In First Out (LIFO) mechanism.

The Crucial Role of the Call Stack in JavaScript Runtime

As you’re well aware, JavaScript is a single-threaded language. Which means it can only perform one task at a time. So how does it remember the flow of your complicated code? Which function called whom and when and what value was returned to which function?

The call stack is used to keep track of all the function calls and their returned values.

Navigating Function Calls in the Call Stack

Say this file was found by the browser :

function a(){
    function b()
}

function b(){
    function c()
}

function c(){
    console.log("Hello world")
}

//Invoking a()
a();
  • The browser first creates a Global execution context and pushes it onto the stack.

  • Then it stores all the function definitions in the variable object.

  • On the last line a() is invoked hence, it is pushed to the top of the Global execution context.

a()
Global Execution Context
  • While executing a(), b() is invoked. Hence, the execution of a() stops midway and b() is pushed on top of a().

  • Similarly, when b() is invoked, c() is called inside b(). So c() will be pushed up on the call stack.

  • console.log() is also a function. What do you think will its behaviour be? If you guessed that it’ll be pushed on top of the stack then you're correct. Congrats nerd, this is an achievement.

Our stack currently looks as below.

console.log(”Hello world”)
c()
b()
a()
Global Execution Context
  • Once “Hello world” is printed on the console, console.log() is popped out of the call stack.

  • The execution of c() is finished hence, it’ll be popped off as well. Let’s look at the stack now.

b()
a()
Global Execution Context (GEC)
  • Now it’s turn for b() and finally a() is popped off as well.

  • So what happens to GEC (Our sole survivor)? Well, it’s popped off as well. Your JavaScript execution has finally been completed.

Phew! Such a hassle for a simple code.


Before we move on, I have one final topic to cover. And that is hoisting. Well, to be honest, this blog was supposed to be solely focused on hoisting. However as I conducted more research I realized, that hoisting would not be an entirely clear topic without understanding the above concepts.

Hoisting

I see this definition a lot on the internet. "Hoisting is referred to as moving the variable declarations and function definitions on top of their scopes". That is only partly correct. It implies that the variable declarations and function definitions are physically moved on top of their scopes.

Now that we know the creation phase in the execution context, we know that JS looks for variable declarations with the var keyword and initializes them as undefined. Similarly, it looks for function definitions and stores them in the variable object.

The declaration code is not moved around anywhere.

Variable Hoisting: With var, let and const

The below code is technically correct but not a recommended practice.

a=10;
b=20;

var a;
var b;

Here a and b are initialized before the declaration. This is possible because of hoisting, in the creation phase, the variables a and b were already available and initialized with undefined.

Now there’s a lot of talk about whether let and const are hoisted or not.

a=10;
b=20;

let a;
let b;

If you try to run the above code, you’ll immediately encounter an error. But Mariya, you said variables can be initialized before the declaration. Yes, but in the case of the let and const keywords, they are block-scoped. Meaning they do not follow the same rules as variables defined with the var keyword.

They are hoisted as well, but live in a specific environment which is called the Temporal death zone. Don’t worry if it sounds like an evil villain of a sci-fi movie. It just means that you can’t use the variables with the let and const keywords before defining them first.

Leverage this quality of let and const keywords to write predictable and manageable code.

Function Hoisting: Declarations vs. Expressions

Building on top of the above example,

a=10;
b=20;

add(a,b)

var a;
var b;

function add(x,y){
    console.log( x + y )
}

The function is invoked before the definition. By now you should have a proper understanding of why this doesn’t result in an error.

The function definition was already stored in a variable object during the creation phase. Hence, when the execution starts for add(), the definition is present.

But wait…

What if we tweaked our code a little and tried the same code with function expression?

Side note - Function expression means storing a function inside a variable.

a=10;
b=20;

add(a,b)

var a;
var b;

var add = function (x,y){
    console.log( x + y )
}

Now this causes error.

Any idea why it happened? All our code is the same and just changing one aspect of it shouldn’t break it?

It does not matter if you stored a function definition in a variable. The variable will be initialized as undefined during the creation phase. When trying to invoke the function, the JS engine tries to find the definition of our function and is met with undefined instead; which causes an error.

The same code will work fine if you invoke the function after the definition.

Debunking myths of hoisting

  1. Hoisting moves variable assignments: One common myth is that hoisting moves the variable assignments to the top of the function or scope. In reality, only variable and function declarations are hoisted, not their assignments.

  2. Hoisting affects the order of execution: Some believe that hoisting changes the order in which code is executed. Hoisting only moves declarations to the top during the creation phase of the execution context, but the assignments remain in their original place.

  3. Variables declared with let and const are not hoisted: Variables declared with let and const are hoisted, but they are not initialized. Accessing them before declaration will result in a "Temporal Dead Zone" error.

  4. Function expressions are hoisted: Function declarations are hoisted, but function expressions are not. Function expressions behave more like variable assignments, and they are not available until the point in the code where they are defined.

  5. Hoisting is a bug: Some developers consider hoisting as a bug or weird behaviour in JavaScript. It's not a bug; it's a deliberate feature of the language designed to facilitate variable and function declaration handling.

  6. Hoisting applies only to global scope: Hoisting applies to all function scopes, not just the global scope. Each function has its own hoisting behaviour, and declarations are hoisted within their respective function's scope.

  7. Hoisting makes code unpredictable: While hoisting can be tricky to understand, it doesn't make code unpredictable if you know how it works. A good understanding of hoisting can help you write more predictable code.


Connecting the Dots: Hoisting, Execution Context, and the Call Stack

All this information might be heavy to grasp at once. So let’s take an example and check what happens at each stage.

console.log("Start of the program");

function foo() {
  console.log("Inside foo");
  bar();
  console.log("Back inside foo");
}

function bar() {
  console.log("Inside bar");
}

foo();

console.log("End of the program");

The sequence of Operations: How Hoisting, Execution Context, and the Call Stack Interact.

  1. Start of the program: Global execution context is created. The program starts executing, and we log "Start of the program" to the console.

  2. foo() is called:

    • A new execution context for the foo function is created.

    • The execution context for foo is pushed onto the call stack on top of the global execution context.

    • Inside foo, we log "Inside foo" to the console.

  3. bar() is called inside foo:

    • A new execution context for the bar function is created.

    • The execution context for bar is pushed onto the call stack.

    • Inside bar, we log "Inside bar" to the console.

  4. bar finishes executing:

    • The execution context for bar is popped off the call stack.
  5. foo continues executing:

    • Inside foo, we log "Back inside foo" to the console.

    • The execution context for foo is popped off the call stack.

  6. The program continues:

    • We log "End of the program" to the console.

In this example, we see:

  • Execution context: Each function call creates an execution context that contains information about the function's scope, variables, and the current point of execution. These contexts are pushed onto the call stack.

  • Hoisting: JavaScript hoists variable and function declarations to the top of their containing scope during the creation phase of the execution context. This is why we can call bar() before its actual declaration in the code.

  • Call stack: It keeps track of the order in which functions are called and their execution contexts. When a function finishes executing, its context is popped off the call stack.


Conclusion: Shedding Light on JavaScript's Inner Working

In this blog post, we've delved into the fascinating world of JavaScript's execution context, hoisting, and the call stack. These concepts are fundamental to understanding how JavaScript code is executed and how functions interact with each other. Let's recap the key takeaways:

  1. Execution Context: Think of it as a container that holds the variables and functions available within a specific scope. It's like your notepad and set of rules for a particular task, guiding the JavaScript engine through the code.

  2. Hoisting: Hoisting isn't about lifting objects; it's about how JavaScript processes declarations during the creation phase of an execution context. Variable and function declarations are moved to the top of their respective scopes, making them available before they're written in the code.

  3. The Call Stack: This critical mechanism maintains the order of function calls and their execution contexts. It operates on a Last-In, First-Out (LIFO) basis, ensuring that functions are executed and returned in the right order.

Understanding execution context, hoisting, and the call stack is vital for writing efficient and bug-free JavaScript code. It not only helps you grasp the inner workings of the language but also equips you to debug code effectively when issues arise. So, the next time you write JavaScript, you'll have a clearer picture of how the code is processed, making you a more confident and capable developer. Happy coding!


Additional Resources: Deepening Your JavaScript Knowledge


About the author

Hi, I am Mariya Zaveri. My passion for JavaSript started when I started learning React before knowing anything about JavaScript. I was frustrated all the time and did not know why JS behaved so weirdly in every situation. That’s when I decided to start exploring the language to its core.

Little did I know that doing this would help me develop a keen interest in web development and JavaScript's intricacies. This blog was born in my attempt to simplify complex coding concepts, making them accessible to both beginners and experienced developers.

My goal is to empower you to become proficient in JavaScript and create amazing web applications. You can connect with me on my Twitter or subscribe to my blog to stay updated with my latest coding adventures and insights.

Did you find this article valuable?

Support Mariya Codes by becoming a sponsor. Any amount is appreciated!