All you need to learn to understand JavaScript đź”® _ Execution Context and Stack
Learn how JavaScript engine evaluates your code.
You may have asked yourself how JavaScript executes the code, determines closure and scope. Might have seen so many weird JavaScript code around hoisting and closures. You may also heard, those are fundamental concepts of JavaScript, and leaning them is crucial. All these concepts come to understanding Execution Contexts and stacks in JavaScript. In this post we will learn all in depth.
The JavaScript Engine
First question you may have is — what is running my JavaScript code? There are two places that you can execute your JavaScript code. It is either inside the browser of your choice, or Node.js. You cannot execute JavaScript code outside these two. Node.js and all browsers use a JavaScript Engine to interpret and execute JavaScript code. There are many JavaScript Engines out there but the two prominent JavaScript engines are: Google V8 and SpiderMonkey. V8 is the Google’s open source JavaScript engine, used in Google Chrome and Node.js and , SpiderMonkey is the Mozilla’s JavaScript engine, used in Firefox. V8 and SpiderMonkey both are written in C/C++.
Now you understand why some certain functions is not supported in certain browsers. Why you should use chrome when you want to use ES6 features but IE does not support does. Because all these browsers run on different engines.
Execution Context
To understand how Javascript Engines runs your code we should first understand the Execution Context (EC) . Every time you run Javascript in a browser (or in Node) breaks down the code into smaller pieces and executes them.
There are three different execution context in JavaScript:
- Global execution context (GEC) — The default environment where your code is executed for the first time.
- Function execution context (FEC)— Whenever the flow of execution enters a function body.
- Eval execution context (EEC) — Text to be executed inside the internal eval function.
We will look into each of these in depth. Let’s start with the Global Execution context.
Global Execution Context
This is the default execution context when JavaScript Engine starts to interpret a JavaScript file either inside a browser or in Node.js. There is only one Global Execution context and it will consist of two things:
- Global object — If you run JavaScript inside browser , the global object is window, otherwise it will set to global inside Node environment.
- this — this will always point to the global object, whether you are inside strict mode or not.
Let’s take a look at an example. For all example here I am using the JavaScript Visualizer to depict execution contexts. I highly encourage you to run these examples and step through them individually.
Here we will run an empty JavaScript file:
You can see that the Execution Context in created with the two properties, window and this. Since we are running the code inside a browser the global object is pointing to the window and this is pointing to the global object which is the window too.
Now let’s look at the example with some code.
When you step through the code you realize they are two distinctive steps happening here. Creation Phase and Execution Phase.
In Creation Phase, JS engine will not execute your code line-by-line, it only:
- creates the global object and this
- set up memory for variables and functions
- initialize all variable with
undefined
and puts function declarations in memory
In Execution phase then of course JS engine starts navigating your code line-by-line, executes it and assign real values to the variable already in memory.
Let’s walk through the above code and see the exact difference between creation and execution phase variable values.
For functions creation phase will assign a memory to their definition so in execution phase you can see the definition is logged out to the console.
In creation phase all variables are allocated in the memory with value of undefined
, then when the execution phase starts executing the first line which logs the value of the variable, it logs undefined
. This is exactly “hoisting”. Hoisting is exactly the result of creation phase, in which variables are assigned the value of undefined
; nothing fancy.
console.log('rabbit is:', rabbit); //rabbit is: undefined
console.log('carrot is:', carrot); // carrot is: undefined
console.log('feedThePet fn is:', feedThePet); // feedThePet fn is: Ć’ feedThePet() {...}var rabbit = 'Lucy';
var carrot = 'carrot';function feedThePet() {
return rabbit + carrot;
}
This process of assigning variable declarations a default value of
undefined
during the creation phase is called Hoisting.
There are so many interview questions around hoisting, but now that you deeply understand what hoisting is, you can answer all. Let’s look at some below:
var x; // declaring x
console.log(x); //output: undefined
if(typeof x === 'undefined') // output: true
Function Execution Context
Function Execution context (FEC)is the second type of executions contexts. Good new is, FEC is exactly the same as the global execution context but it gets created whenever a function is invoked.
Below we are just calling the function we have created. You can see the new Function Execution context is getting created with the same phases as the global execution context. This new context exists inside the global context and will has access to variables in its parent context ( the global context in this example).
You may ask what is the difference between the Global Execution Context and the Function Execution Context then?
Well, as we described below are what GEC do on creation:
- Creates the global object
- Create an object called this
- Sets up memory for variables and functions
- Initializes all variable with
undefined
and puts function declarations in memory
The difference is Function Execution Context does not need to create the global object again, since as it comes from its name, the global object is global and needs to get created once. Instead the Function Execution Context creates another object called arguments.
So, here is what Function Execution Context does on creation:
- Creates the arguments object
- Create an object called this
- Sets up memory for variables and functions
- Initializes all variable with
undefined
and puts function declarations in memory
As you realized in the example above, the Function Execution Context is getting removed after execution, but in reality JS engines uses an Execution Stack or Call Stack, anytime a function is invoked, it gets put into the Execution Stack, and when it is finished executing, it will get removed from the Execution Stack. We will deep dive into execution stacks later on.
Now let’s make our example more complex and add some local variables to our function:
In above example we have num1 as a global variable and our function has num2 as a local variable. It is important to note that num2 only exists as long as the function is executing. num2 is inside the function execution Context since it was defined there, not the global context. So, when the function is done executing, num2 does not exist anymore. This is an important step stone to understanding the next topic, which is Scope.
Scope
The current context of execution. The context in which values and expressions are “visible,” or can be referenced. -MDN
This is how MDN defined the Scope. Scope of a variable is where its execution context is. Let’s look at a simple example below. What do you think the value of foo is logged?
function fn() {
var foo = 2;
}fn();
console.log('foo is: ', foo);
Let’s walk this through the JavaScript Visualizer:
As you can see, the variable foo is a local variable inside fn’s Function Execution Context. So foo’s scope is only inside fn function. That means as soon as fn is popped out of the execution stack , foo will be accessible. So, we got a ReferenceError in the console indicating foo is not defined.
Let’s run a more complicated code through the visualizer, to understand scope better.
In the example above, initially we have foo as 10 and bar as 3 in the global scope. As soon as we invoke the anonymous function, we create a Function Execution Context which has foo as its local variable foo. Note that this is not the same as the foo in the global scope. The function also assign 1 to bar, but we cannot see the bar in the function execution context.
What does JS engine do when it cannot find the variable in the current execution context (scope) ?
Well, it looks at the parent scope and if it does not find it there either, it keeps looking up until it reaches the global scope.
In our example, bar does not exist in the function execution context, but it does exist in the parent execution context (scope). That is why the bar value changes to 1 in the global scope. This is called Scope Chain.
This process of the JavaScript engine going one by one and checking each individual parent Execution Context if a variable doesn’t exist in the local Execution Context is called the
Scope Chain
. JavaScript Visualizer shows the Scope Chain by having each new Execution Context indented and with a unique colored background. Visually you can see that any child Execution Context can reference any variables located in any of its parent Execution Contexts, but not vice versa.
The foo value remains the same in our example, since after the function is done executing, the local foo scope is over. Hence the new bar value is11 at the end.
Closures đź’Ą
Now let’s look at one more concept here, closures.
A closure is a feature in JavaScript where an inner function has access to the outer (enclosing) function’s variables — a scope chain.
The inner
function can access the variables of the enclosing function due to closures in JavaScript. In other words, the inner
function preserves the scope chain of the enclosing function at the time the enclosing function was executed, and thus can access the enclosing function’s variables.
In our example the sum function is returning an inner
function which that returns the summation of a, b and e. All that means is the inner function can access the variable of its outer scope (outer function, enclosing scope). So the inner
function is able to access a which is inside the closure scope.
Execution Stack
Javascript is a single threaded single concurrent language, meaning it can handle one task at a time or a piece of code at a time. It has a single call stack which along with other parts like heap, queue constitutes the Javascript Concurrency Model (implemented inside of V8).
To understand the Execution Stack fully let’s visualize the following code using Loupe ( JS Call Stack Visualizer).
function foo(b) {
var a = 10;
return a + b + 11;
}
function bar(x) {
var y = 3;
return foo(x * y);
}
console.log(bar(7)); //returns 42
In the above, it starts from console.log(bar(6)), which is pushed on to the stack, next frame on top of it is the function bar with it’s arguments which in turn calls function foo which is again pushed on to the top of stack and it returns immediately and so is popped out of stack, similarly bar is then popped out and finally console statement is popped out printing the output.
These were are essential concepts you needed to understand how JavaScript evaluates your code under the hood. For me personally knowing these made it so easy to grasp hoisting, scopes, closures and call stack. Hope, you like it, feel free to post your valuable comments, to help me improve.