< WebDevRef />

Embracing JavaScript: Blank script, Execution Context and the Call Stack

Sep 14, 2021

#javascript

6 min read

last updated: Sep 17, 2021

Just like any artist, we start with the blank canvas

So, this is our HTML file.

<!DOCTYPE html> <html lang="en"> <head> <!-- meta tags -- > <title>JS Playground</title> <script src="app.js" ></script> </head> <body> </body> </html>

We've got the HTML shell, and a script "app.js", which looks like:

// app.js

Yep, it's blank. But guess what, the JS engine has already started working.

The moment the script gets loaded, the engine gets to work, and the first thing it does is: Create an Execution Context.

Context basically refers to the space/environment in which the JS engine evaluates and executes a piece of code. (quite literally how we phrase, "in the context of")

JS engine reads our code line by line, weaves through it like a single thread and executes it, that's why we call JavaScript a single-threaded, synchronous programming language.

πŸ”₯ Execution contexts are of following three types:

  • Global Execution Context (default)
  • Function Execution Context
  • Eval Execution Context (need not bother for now)

In the above scenario, with a blank script, we get the default execution context, i.e. the Global Execution Context.

Let's start painting now

πŸ”₯ Now, there are two phases while creating an execution context:

  1. Creation Phase
  2. Execution Phase

1. Creation Phase

In this phase, we go another level deep into something called a Lexical Environment.

In this environment, JS does a spell, called Hoisting. It scans through our code line-by-line and captures all the variable and function declarations, allocates them some space in the memory and initializes them with some values.

What those values are?

For primitive values, it's

  • "undefined" if variable declared using "var"
  • left uninitialized if variable declared using "let" or "const"

For functions, the entire function definition is stored

πŸ”₯ Going deeper, we've got three primary components under lexical environment:

  • Environment record
  • Outer environment reference
  • "this" keyword binding

πŸ“Œ Environment Record

Now environment record in itself has two types: Declarative and Object. You can read more here.

All in all, it basically keeps a record of all the variable and function declarations. This record is a reflection of "Hoisting" we've briefed above.

πŸ“Œ Outer environment reference

As your code grows, so does the variables, functions and nesting. So, JS makes sure that every Execution Context has a reference to its surrounding, in case some variable is outside its own scope (we'll discuss in later posts).

let item = "Tesla"; function elonSays() { console.log(`Go buy a ${item}`); }

"elonSays" function has reference to its outer lexical environment. And since the "item" variable is not in function scope, so JS knows to look for the "item" variable in the outer environment which happens to be the global context in this case.

*For a function, the outer reference will be the "Global Context", however for the global context itself, it will be "null" since nothing sits over it. *

πŸ“Œ "this" keyword binding

Now, the "this" keyword need separate discussion altogether. Let's keep it simple for now.

"this" keyword refers to an object. Now what that object is, depends on the context. Consider this πŸ˜‚ example:

// 1. global context console.log(this); // displays the "window" object // 2. function called in global context function whatIsThis() { console.log(this); // displays the "window" object } // 3. function as an object property let agent = { name: "Jason Bourne", showPosition: function () { return this; }, }; console.log(agent.showPosition); // displays the "agent" object

2. Execution Phase

All those hoisted variable will be assigned corresponding values, and the function(s) will be executed.

var character = "Draco Malfoy"; function greetCharacter() { console.log(`Hello, ${character}`); } greetCharacter(); // upon hoisting // var role = undefined; // entire greetWorld function definition stored in memory // during execution phase // var role = "Draco Malfoy"; // executes the function and prints "hello world"

πŸ˜‡ Let's summarize the whole flow with an example.

let characterOne = "Sirius Black"; showFavoriteCharacter(); function showFavoriteCharacter() { let characterTwo = "Severun Snape"; console.log(`Everyone loves ${characterOne} and ${characterTwo}`); }

Global Execution Context

Creation phase

  • Environment record (hoisting occurs)

let characterOne; // uninitialized

showFavoriteCharacter //holds the entire function definition

  • outer env reference: null
  • "this" keyword: window object

Execution phase

  • Environment record let character = "Sirius Black";
  • outer env reference: null
  • "this" keyword: window object

Now, as we hit the "showFavoriteCharacter" function, a totally new execution context will be created.

Function Execution Context

Creation phase

  • Environment record (hoisting occurs)

let characterTwo; // uninitialized

  • outer env reference: Outer Lexical Environment
  • "this" keyword: window object

Execution phase

  • Environment record

let character = "Severus Snap";

  • outer env reference: Outer Lexical Environment
  • "this" keyword: window object

With that outer reference, the function is able to locate the "characterTwo" variable in the global context. And finally, it logs: "Everyone loves Sirius Black and Severus Snape"

This whole process of creation and execution occurs for each and every function that gets called throughout your script, no matter how nested it gets.

Β 

πŸ€” But, wait a minute. How does JavaScript handle those bazillion execution contexts, that too with deep nesting?

πŸ”₯ Well, JS maintains the flow of execution using the Execution Call Stack, or simply Call Stack. A stack basically follows the LIFO principle i.e. Last In First Out.

The process goes like this:

  • When the script runs, default Global Execution Context (GEC) gets created and pushed to the stack
  • When it finds a function call/invocation, a Function Execution Context (FEC) gets created and pushed on top of GEC.
  • And if that function calls another one, a new EC gets created and pushed on top of it. Once the function returns or the execution finishes, the stack is cleared starting from the top.
outer(); function outer() { //some instructions inner(); function inner() { // some instructions } }

During the execution phase, the call stack would look like this,

[2] inner()

[1] outer()

[0] GEC

Once each function execution finishes, its FEC popped off the stack, and ultimately the GEC is removed as well.

And there you go. Hope you got some clarity on how the execution stack is created and handled.

Β 

πŸ“šπŸ’‘ Resources referenced for this blog:

Β 

πŸ•Teaser

Did you see how we've called outer() function even before defining the function itself. Well, it's all because of Hoisting and Scope. Topics that we'll discuss in the upcoming posts.

Β 

Until then, if you have any queries, suggestions or corrections, drop a comment below.

Have a good one. Take care. Bye. πŸ‘‹

Go to all posts...