const - JS | lectureHow JavaScript Works Behind the Scenes

Execution Contexts and The Call Stack

How JavaScript Works Behind the Scenes

In this lecture, we will learn how JavaScript code is executed. We already know that it happens in the call stack in the engine from our last lecture, but let's dig a bit deeper now.

Let's assume that our code just finished compiling, .i.e, our code is now ready to be executed. Then, a so-called global execution context is created for the top-level code. Top-level code is code that is not inside any function.

So, only code outside of functions will be executed in the beginning. This makes sense because functions should only be executed when they are called. We saw this happening already in our pig game project, where we had an init function that initialized our entire project. Still, to initialize the game the first time the page loaded, we immediately needed to call that function in our top-level code.

Let's have an example to illustrate it better.

script.js
const name = 'Ulrich';
const first = () => {  let a = 1;
  const b = second();
  a = a + b;
  return a;
};

function second() {  let c = 2;
  return c;
}

From the above code, the name variable declaration is top-level code, and therefore it will be executed in the global execution context. Next, we have two functions, one expression and one declaration. These functions will also be declared so that they can be called later. But the code inside of these functions will only be executed when the functions are called.

We now know that a global execution context is created for top-level code. Let's now understand what exactly is an execution context.

An execution context is an abstract concept, but we can define it basically as an environment in which a piece of JavaScript code is executed. It's like a box that stores all the necessary information to execute some code, such as local variables or arguments passed into a function.

To make this a bit more intuitive, let's imagine you order a pizza at a takeaway. Usually, that pizza comes in a box, and it might also come with some other necessary stuff for you to eat the pizza, such as cutlery or a receipt so that you can pay for the pizza before eating it.

In this analogy, the pizza is the JavaScript code to be executed, and the box is the execution context for our pizza. That's because eating pizza happens inside the box, which is then the environment for eating pizza. The box also contains cutlery and the receipt, which are necessary to eat a pizza or, in other words, to execute the code.

I hope that made sense and made the concept of execution context a little bit more clear.

No matter how large it is, there is only ever one global execution context in any JavaScript project. It's always there as the default context, and it's where top-level code will execute. Speaking of execute, now that we have an environment where the top-level code can be executed, it is finally executed. Once the top-level code is finished, functions finally start to execute as well.

A new execution context will be created for every function call containing all the necessary information to run exactly that function. The same goes for methods because they're simply functions attached to objects.

All these execution contexts together make up the call stack we mentioned before. When all functions are executed, the engine will keep waiting for callback functions to arrive to execute these, such as a callback function associated with a click event. Remember that it's the event loop that provides these new callback functions as we learned in the last lecture.

Here is an image summarizing what we just talked about.

Global Execution Context

Now that we know what an execution context is, let's find out what it's made of. The first thing that's inside any execution context is a so-called variable environment. All our variables and function declarations are stored in this environment, and there is also a special arguments object.

As the name says, this object contains all the arguments passed into the function that the current execution context belongs to. Remember that each function gets its own execution context as soon as the function is called. So basically, all the variables that are declared inside a function will end up in its variable environment.

However, a function can also access variables outside of the function. This works because of the scope chain, and we will learn all about scoping and the scope chain later. But for now, what you need to know is that the scope chain consists of references to variables that are located outside of the current function, and to keep track of the scope chain, it is stored in each execution context.

Finally, each context also gets a special keyword variable called this. There will be a special lecture just about the this keyword later.

The content of the execution context, that is, the variable environment, the scope chain, and the this keyword, is generated in a so-called creation phase which happens right before execution.

One very important detail that we need to keep in mind is that execution contexts belonging to arrow functions do NOT get their own arguments keyword, nor do they get the this keyword.

Basically arrow functions don't have the arguments object and the this keyword. Instead, they can use the arguments object, and the this keyword from their closest regular function parent. This is an extremely important detail to remember about arrow functions, and we will come back to it later.

Below is an image to summarise.

Content of Execution Context

Now behind the scenes, it's actually even more complex but I think we're fine like this. Let's try to simulate the creation phase for the below code example.

script.js
const name = 'Ulrich';

const first = () => {
  let a = 1;
  const b = second(7, 9);
  a = a + b;
  return a;
};

function second(x, y) {
  let c = 2;
  return c;
}

const x = first();

We will get one global execution context and one for each function. That is one for the first function and one for the second function.

Execution Context Simulation

In the global context, we have the name variable declaration, the first and second function declarations, and the x variable declaration.

For the functions, the variable environment will contain all the code of a particular function. The value of x is marked as unknown above because its value is the result of the first function that we didn't run yet.

Technically none of these values become known during the creation phase, but only in the execution phase. So this is not 100% accurate, but it's just to illustrate how these execution contexts work.

Now in the first function, we have the a variable set to 1 and the b variable which once again requires a function call to become known. Finally, the variable environment of the second function, contains the c variable set to 2 and since this is a regular function, so not an arrow function, it also has the arguments object. This object is an array that contains all the arguments that were passed into the function when it was called. In this case, as you can see below, that's 7 and 9.

Execution Context Simulation 2

Doing this feels simple because this is an extremely small amount of code. Imagine there are hundreds of execution contexts for hundreds of functions. How will the engine keep track of the order in which functions were called? And how will it know where it currently is in the execution?

Well, that's where the call stack finally comes in. Remember that the call stack and the memory heap make up the JavaScript engine itself. The question now is, what is the call stack?

A call stack is a place where execution contexts get stacked on top of each other to keep track of where we are in the program's execution. So the execution context that is on top of the stack is the one that is currently running. When it's finished running, it will be removed from the stack, and execution will go back to the previous execution context.

Call Stack

Using the analogy from before, it is as if you bought pizzas with some friends. Each friend has a pizza box, and then you put the boxes on top of each other, forming a stack to keep track, which pizza belongs to each friend.

Yeah! It feels a bit abstract; I get it! Let's have an example to demonstrate how it works. We will use the above code together to show you precisely what happens.

Once the code is compiled, top-level code will start execution. As we learned at the beginning of the lecture, a global execution context will be created for the top-level code. Again, this is where all the code outside of any function will be executed.

This global execution context will be added to the call stack. Since this context is now at the top of the stack, it is the one where the code is currently being executed.

Call Stack Execution

Moving on, we have a simple variable declaration, then the first and the second functions are declared. The last line is where things start to get interesting. We declare the x variable with the value that will be returned from calling the first function.

script.js
const x = first();

Let's call that function. As you already know, when we call a function, it gets its own execution context to run the code inside its body. This context then gets added to the call stack, on top of the current context, and so it's now the new current execution context.

Call Stack Execution First

In our first function, we have yet another simple variable declaration, and this variable will be defined in the variable environment of the current execution context and not in the global context.

Then right in the next line, we have another function call. Let's call that function and move there. As you guessed, a new execution context was created right away for the second function. Once more, it is added onto the call stack and becomes the new active context.

Call Stack Execution Second

What's important to note here is that the execution of the first function has now been paused. We are running the second function now, and in the meantime, no other function is being executed. The first function stopped when the second function was called and will only continue as soon as the second function returns. It has to work this way because, remember, JavaScript has only one thread of execution, and so it can only do one thing at a time. Never forget that.

Call Stack Execution All

Now, moving to the next line, we have a return statement meaning that the second function will finish its execution.

This means that the function's execution context will be popped off the stack and disappear from the computer's memory. At least that's what you need to know for now because the popped-off execution context might keep living in memory. More about that later.

Call Stack Execution Second Pop

Next, the previous execution context will now be back to being the active execution context again. Let's also go back to where we were before in the code. I hope that by now, you start to see how the call stack keeps track of the order of execution here.

Without the call stack, how would the engine know which function was being executed before? It wouldn't know where to go back to, right? And that's what makes the beauty of the call stack. It makes this process almost effortless.

It's just like a map for the JavaScript engine because the call stack ensures that the order of execution never gets lost. Just like a map does, at least if you use it correctly 😅.

Let's get back to our code. We returned from the second function, and we are now back in the first function, where we have a calculation and then the return statement of the first function.

After the return statement is executed, the same as before happens. The current execution context gets popped off the stack, and the previous context is now the current context where code is executed.

Call Stack Execution First Pop

In this case, we're back to the global execution context and the line of code where the first function was first called.

So here, the return value is finally assigned to x and the execution is finished. The program will now actually stay in this state forever until it is finally really finished. That only happens like when we close the browser tab, or the browser window.

Only when the program is really finished then the global execution context is also popped off the stack.

This is, in a nutshell, how the call stack works. Hopefully, it makes sense now that Javascript code runs inside the call stack. It is more accurate to say that code runs inside of execution contexts that are in the stack. But the general point is that code runs in the call stack.

In the next lecture, we will learn more about the variable environment and how variables are created. So stay tuned for the next lecture.