Revisiting the JavaScript Engine
In the previous article, we had a peek inside the JavaScript engine. We learned from the article that all JavaScript engines must have a call stack and a memory heap.
The memory heap is where objects are stored; think of it as a drawer to keep items. On the other hand, the call stack keeps track of the execution contexts.
The Execution Context
When the JavaScript engine executes code, it creates an environment for it to run. This environment is called an execution context, and it stores the data necessary for the code to execute, for example, the variables declared. The JavaScript engine adds this execution context to the call stack, which keeps track of all the execution contexts created.
Call stacks operate on the idea of last-in-first-out. That is, the last item added to the call stack is the first item removed. Think of clearing the table after a meal. You will take one dirty plate and then add another dirty plate on top and so on until you have a stack of plates. When it comes to washing the dishes, the plate added last would be the plate cleaned first.
The Global Execution Context
When the JavaScript engine executes your code, the first execution context created is the global execution context. This execution context is for all the top-level code, that is, the entire script, and thus there is only one global execution context in a program. The global execution context is the first item added to the call stack and becomes the active task.
Every execution context has two phases: the creation phase and an execution phase.
During the creation phase of the global execution context, the JavaScript engine will:
- Create the global object (the global object is
window
in the browser andglobal
in Node.js) - Create
this
keyword with a binding to the global object
Let's examine the global object
and this
keyword by creating an HTML file and linking an empty JavaScript file. Because the creation phase happens before execution, the global object
and this
keyword will be available even in an empty script. Open this HTML file in the browser and inspect it using the DevTools. Type window
in the console tab and hit enter, and do the same for this
keyword.
From the image above, you can see that window
and this
keyword are the same.
The next phase of the global execution context is the execution phase, and this involves the JavaScript engine running the code line by line. When the JavaScript engine encounters a function call, it creates a new execution context in which the function code will run.
Function Execution Context
Whenever the JavaScript engine executes a function, it creates a new execution context that will hold the data necessary for the function to run. The JavaScript engine will add this execution context to the call stack, which becomes the active task.
If this function calls another function within its body, the JavaScript engine will create another execution context with the data necessary for the called function to execute. The JavaScript engine will add the called function's execution context on top of the calling function's execution context. The called function will become the active task, and the JavaScript engine will pause the execution of the calling function until the called function completes.
When the called function returns or completes, the JavaScript engine will remove its execution context from the call stack, and the calling function will resume execution.
The function execution context also has a creation phase and an execution phase. During the creation phase, the JavaScript engine will:
- Set up the scope chain
- Create
this
keyword - Set up a variable environment that will contain functions defined inside the function, variable declarations (var, let, and const), and the arguments object
We will look at each of the above in detail in separate articles, but you should note that arrow functions don't get their own arguments object and this keyword. They get these from their closest regular functions.
The next phase of the function execution context is the execution phase, and all the code in the function will be run line by line.
The Call Stack and Execution Context in Action
Let's look at an example to understand the call stack and execution context clearly. Consider the code below:
//debugger // uncomment this line to visualize the code using the DevTools
const username = 'Paul';
const birthYear = 2000;
function printFormattedName(username, birthYear) {
const age = getAge(birthYear);
console.log(`My name is ${username} and I am ${age} years old`);
}
function getAge(birthYear) {
return new Date().getFullYear() - birthYear;
}
printFormattedName(username, birthYear);
The image below describes how the call stack and execution contexts work:
- When the JavaScript engine executes the code, it creates the global execution context and adds it to the call stack. During the execution phase, it encounters the printFormattedName() function call.
- When the JavaScript engine calls the printFormattedName() function, it creates a new execution context and adds it to the call stack. During the execution phase, a call to getAge() function occurs.
- When the JavaScript engine calls the getAge() function, it creates a new execution context and adds it to the call stack. Execution of the printFormattedName() function is paused. During the execution phase of the getAge() function, it returns the calculated age. This function completes.
- The JavaScript engine removes the getAge() execution context from the call stack and resumes execution of the printFormattedName() function. This function also completes.
- The JavaScript engine removes the printFormattedName() function from the call stack and resumes execution of the global execution context. The global execution context will stay on the stack until the program closes.
It might also be helpful to visualize the code using the developer tools. Run your code with the DevTools open and uncomment the debugger on line 1 and reload the page. You should see a page similar to the one below. While this article isn't about the DevTools, I would encourage you to go through the Google tutorials on the DevTools if you aren't familiar with the DevTools. Using the DevTools, we can see how the call stack grows with function calls and the variables and functions accessible.
Discussion Questions
- We've seen that calling a function adds a new execution context to the call stack. What would happen if we kept adding execution contexts to the call stack indefinitely?
- JavaScript is a single-threaded language; of what benefits is the call stack in JavaScript?