Unearthing JavaScript: Execution Contexts and Hoisting

Kartik Srivastav
7 min readAug 21, 2023

--

JavaScript is, without a doubt, an exciting and quirky language. It comes with various concepts that seem complicated but are easily understandable if looked upon from a different perspective. Conversely, some phenomena which seem simple at first sight can be misinterpreted easily.
Here, we will look at such concepts and understand what is happening behind the scenes of JavaScript. Let’s look into the ideas of Execution Contexts and Hoisting.

Necessary Groundwork and Key Terms

Before learning about the Execution Contexts, there are some terminologies that we need to understand.

  1. Syntax Parser: Syntax Parser is a program that reads your code and determines what it does and whether its grammar is valid.
  2. Lexical Environments: A lexical environment is a context where a piece of code is located. Where you write something in your code affects its execution, e.g., the scope of variables and functions.
  3. JavaScript Objects: These are one of the fundamental building blocks of the language itself. Essentially, these are just a collection of Key/Value pairs where a Key maps to a unique Value, and the Value further can be more Key/Value pairs.

Execution Contexts

We talked about lexical environments, and there are several lexical environments in a JavaScript code. How do we know which one is currently running? How is it being managed?

The concept of Execution Contexts answers the questions mentioned above. An Execution Context is a wrapper in which the code is executed and managed. It tells which lexical environment’s code is currently being executed, and it also manages variables, functions, and other resources that the specified piece of code works with. Some points to note about Execution Contexts:

  • Whenever a code is executed in JavaScript, it runs inside an execution context.
  • It can also contain elements you have not written in your code!
  • Every function invocation creates a new execution context for the function called in the execution stack.

Execution contexts in JavaScript are created in two phases: the Creation phase and the Execution phase. Before learning about these two phases, let’s look at an example of the execution context with the help of the Global Environment and Global Object concept.

The Global Environment and Global Object

The base execution context when a JavaScript Code is run is the global execution context. When discussing JavaScript, “global” means “not inside a function”. It sets up some special elements for us:

  • The Global Object: Let’s say we run a basic HTML code with an empty JavaScript source file attached to it on our browser; we would see a blank webpage and if we open our browser’s DevTools console, it would be empty as well. Now, if you type “window” in the console and hit Enter, you will get a window object with several properties and methods. Well, now you may wonder, we didn’t write any JavaScript code, then where did this window object come from?
    The JavaScript engine creates this window object, the global object of a browser’s JavaScript environment. Every new tab you open in your browser has a unique window object, which is one of the things that exist in the base execution context.
    Now, if you add some code to the empty JavaScript file, variables and functions, not inside a function will be attached to the window object as properties and methods.
    NOTE: If you are running your code inside a NodeJs server, you may have a different global object instead of the window, but there will always be a global object.
window object output in a browser’s console
  • this: Another thing that is created in the base execution context by JavaScript is a special variable called “this”. At the global level, this is equal to the global object (window for browsers).
“this” keyword output in a browser’s console
  • Outer Environment: Every execution context has an outer environment which is the lexical environment that encloses the execution context. However, as the global context is the outermost context you can be in, the Outer Environment in the base execution context is null.
Global Execution Context

Phases of Execution Context and Hoisting

Let’s take a look at the following piece of code and its output and try to understand what is happening:

console.log(num);// Printing num
var num = 5;// Initializing num

func();// Calling func
function func() {
console.log("I am inside func!");
}


/* Output:
undefined
I am inside func!
*/

Weird, Right? There are two observations to note:

  1. We called the function “func” before its definition and it still ran properly.
  2. We tried to print the value of “num” before its initialisation and it gave the output undefined.

To understand this weird phenomenon, let’s look at the first phase of the execution context, the Creation Phase:

Elements like Global Object, Outer Environment, and “this” variable are set up in the creation phase. During this creation phase, the Syntax Parser scans your code and recognises where you have created functions and variables. It then sets up the memory space for these variables and functions, and this setting up of memory space is called Hoisting. Yes, you read that right, this is the actual Hoisting, and though many people confuse it with the code being moved to the top, there isn’t any actual physical movement of the code happening.

So then, why does the function call work properly? It’s because, during this creation phase, functions are loaded into memory in their entirety. So, even when we try to access them before their definition, we don’t get any errors. On the other hand, the variables are just given a memory space and assigned a placeholder value. Can you guess that placeholder value? Yes, it is the undefined keyword, the special value assigned to a variable when it is declared but is not assigned any other value yet. So when we try to access a variable before it is declared, its value will be undefined. Note that this memory space allocation only happens for declared variables in your code. If you try the same thing as above without declaring the variable like the following code, you will get an error: “Reference Error: num is not defined”.

console.log(num);// Printing num

/* Output:
Uncaught ReferenceError: num is not defined
*/

Also, note that the Hoisting described above works with variables declared with the var keyword. Hoisting with let and const keywords works differently, as explained later below.

So when does the value mentioned in the code actually gets assigned to the variable? This happens in the second phase of the execution context, the Execution Phase:

In this phase, we already have the things set up in the first phase. Now the interpreter goes over your code line by line and executes what the code intends, assigning values to variables, invoking functions, and performing other operations.

Global Execution Context During Creation and Execution Phase

Hoisting with “let” and “const” variables

Let’s try doing a similar thing with a let variable while also demonstrating a difference between declaring and defining a variable:

console.log(num);// Printing num
let num;// Declaring num
num = 5;// Defining num

/* Output:
ReferenceError: Cannot access 'num' before initialisation
*/

Here we don’t get undefined as an output. Instead, we get an error: “ReferenceError: Cannot access ‘num’ before initialisation”, which is different from the one we got previously. That’s because the let keyword works differently. let and const variables are hoisted without a default placeholder value, unlike var variables. Now let’s change the above code a bit and see the difference:

let num;// Declaring num
console.log(num);// Printing num
num = 5;// Defining num

/* Output:
undefined
*/

So here we get output as undefined because now, before accessing the variable, we have declared it, and that is the difference we see when code is actually physically moved to the top(or anywhere else before accessing a specific variable) because if while hoisting the code of variable declarations was actually being moved to the top, we would have got undefined as an output of the code previous to the above one instead of an error.

const variables act almost the same as let variables. The difference is if you try any of the above two codes with const variables, for example, like this:

const num;// Declaring num
console.log(num);// Printing num
num = 5;// Defining num

/* Output:
SyntaxError: Missing initialiser in const declaration
*/

We will get the error: “SyntaxError: Missing initialiser in const declaration” in both cases because const variables require you to initialise them with a value at the time of declaration, and that’s why you won’t be able to get undefined as an output.

Final Thoughts

The concept of Hoisting can be easily misunderstood, and understanding Execution Contexts can be really helpful when trying to understand actual Hoisting. Many scenarios mentioned above can cause unforeseen problems or errors while writing code if they are not appropriately understood. We can follow some safe practices like using let and const variables instead of var variables or applying the ESLint rule “no-use-before-declare” while coding to avoid such errors. But it is still interesting to know about such concepts and learn about the deep secrets of your favourite programming language.

Thank you for reading this article. I hope it was worth it and you learnt something fabulous today. Feel free to share your thoughts in the comments :).

I sincerely thank @anthonypalicea for enhancing my understanding of some of the JavaScript concepts mentioned in this article through his online course.

--

--