In this article, you’ll learn how JavaScript is executed in the browser. The browser uses JavaScript runtime (like V8 in case of Google Chrome), CallStack, Memory Heap, Callback Queue and Event Loop for executing JavaScript. JavaScript is designed to be operated as a single-threaded language, yet we can execute asynchronous JavaScript code in the browser without blocking the main thread. This article aims to provide a detailed explanation of the underlying tools used by the browser to execute JavaScript code. You will also learn how asynchronous code is executed with the help of the Event loop. If you follow along with this tutorial, some of the core concepts in JavaScript, including its asynchronous nature, closures, hoisting and scope chain, will make complete sense!
This article is part III of the series on The Journey of JavaScript: From Downloading Scripts to Execution. Before we dive into the execution part of JavaScript, let’s recap all that we have learned in the previoustwo articles of the series:
- The
script
tags are blocking in nature. When the main thread encounters ascript
tag, it blocks the rendering of the HTML DOM and gets busy in downloading, parsing and executing these scripts. In the first part of the series, we learned the various ways of downloading scripts without blocking the main thread. We also saw some of the heuristics employed by the V8 JavaScript engine to speed up the parsing phase. - In part II of the series, we dove deep into understanding the internals of JavaScript engines. The engines make use of Just-In-Time Compilation to produce the intermediate bytecode. The bytecode is then further optimized by the optimizing compiler.
I highly recommend reading the previous two parts of the series before jumping into the execution section, as they build on the fundamentals of JavaScript engines.
Table of Contents
- Execution Context
- Terminologies Related to Execution Context
- Scopes and Scope Chain
- Creation Phase
- Activation Phase
- Inferences from our Understanding of the Two Phases
- callstack and memoryheap
- What Is an Asynchronous Code and How Is It Executed
- Event Loop
Let’s review some of the essential concepts that are required for understanding the execution of JavaScript:
Execution Context
Execution Context can be viewed as a container that holds variables and functions. Let’s consider a simple example to understand more:
var a ='Hello World!'functionhelloWorld(){var a ='Hello Function!'
console.log('Value of a inside the function helloWorld', a)// Prints Hello Function!}helloWorld()
console.log('Value of a outside of the function helloWorld', a)// Prints Hello World!
This is simple and straightforward code in JavaScript, but it is very important to understand how the above code will be executed by the JavaScript engine. When the above code is getting ready for execution, the main thread first creates a big container for the entire program. This container is called the Global Execution Context. It stores all the variables and functions that are required during the lifetime of the entire program. The below diagram reflects the state of the global execution context:
In JavaScript, every function operates in its own execution context. Let’s try to fit this statement in the container analogy–every function in JavaScript along with its local variables fit in another small container inside the main container. The smaller container can access the variables of the big container; however, the bigger containers cannot access variables of the inner smaller containers.
Let’s visualize the execution context for helloWorld
function:
Please note: the inner smaller containers give preference to their local content. They first check if the variable in question is present in their specified range, and if it is not present they ask for it from its parent container.
If you have understood the container analogy for the execution context, the output of the console
statements in the above code should seem obvious to you! The value of a
inside the helloWorld
function is Hello Function!
. Outside of it, the value is Hello World!
because the helloWorld
function has its own execution context that holds the variable a
with value Hello Function!
. The variable a
inside the helloWorld
function is not accessible outside of it, and hence its value is Hello World!
in the global execution context.
Terminologies Related to the Execution Context
The main block in which the entire program sits is called the Global Execution Context. Every function runs in its own context and it is called a Function Context or Local Execution Context. The recent advancement in JavaScript with ECMAScript version 6 (ES6) allows maintaining Lexical Execution Context or Block-level Execution Context with the use of let
and const
.
Let’s understand the lexical execution context with the help of an example:
for(let i =0; i <5; i++){
console.log('value of i inside the for loop', i)}
console.log('value of i outside the for loop', i)
The variable i
is not defined outside of the for
loop because of the use of let
creates a block-level execution context and its lifetime is the closing bracket }
. The execution context created by i
is called as the Lexical Execution Context.
Scopes and Scope Chain
We learned about parsing in the first part of the series. The job of the Parser is to create Scopes and the Abstract Syntax Tree (AST). We have already learned about AST in detail, and now let’s see what scopes are and how they’re created.
Scopes define the area of operation of a particular variable. A variable defined in the global executed context is scoped globally and can be accessed by any function in the program, and the variables defined in a local execution context are scoped to the local level. So, we have global, local and lexical scopes.
Let’s move forward to Scope Chains:
The local functions have access to their local scope as well as the global scope. If there is a nested function inside this local function, it will have access to its own scope, its parent’s scope, and the global scope. This forms a chain of scopes and hence it is called a Scope Chain. Let’s see this in action!
var a =1functionfoo(){var b = a +1functionbar(){var c = b + a
}bar()}foo()
When we execute the above code, the interpreter creates a scope for function foo
and its scope chain would look something like [foo scope, global scope]
. The scope for function bar
would be [bar scope, foo scope, global scope]
.
The function bar
has access to the variables of its outer function bar
because of the scope chain. Can we infer something interesting out of it? Let me quote the definition of Closures
, which should now make complete sense:
A closure is the combination of a function and the lexical environment within which that function was declared. - MDN
The use of the word combination
should be obvious, as it combines the scopes of inner and the outer functions to form its own scope chain. I hope you now know that a closure is not magic but a combination of scopes.
Look-ups on Scope Chain
When the interpreter encounters a variable while executing a piece of code, it first looks for its value in the current scope. It traverses up the scope chain until the variable is found or it has reached the end of the scope chain.
Look-ups on the Prototype Chain
Let’s see an example to understand how scopes work with prototype chains:
var obj ={}functionbaz(){
obj.a ='Hello'returnfunctionbar(){
console.log('Property a set on obj', obj.a)}}var bazReturnFn =baz()bazReturnFn()// Prints Property a set on obj, Hello
The above code is straightforward. The function baz
returns an anonymous function. The variable bazReturnFn
stores the return value of the function baz
. The function bazReturnFn
runs in the global context. It finds the variable obj
in the global context and then searches for the property a
on obj
.
Let’s define the property a
on the prototype of obj
:
var obj ={}functionbaz(){
obj.prototype.a ='Hello'returnfunctionbar(){
console.log('Property a set on obj', obj.a)}}var bazReturnFn =baz()bazReturnFn()// Prints Property a set on obj, Hello
It still returns the same value! Please note the interpreter first traverses the whole scope chain and then goes to the prototype chain for finding the value of a variable.
Now that we’re clear with the terminologies, let’s get to the two important phases involved in the execution of JavaScript.
Creation Phase
When a function is called, the interpreter creates its execution context and then the function enters the creation phase. The interpreter traverses the function code line by and line and does the following:
- Creates the scope chain
- Creates the variables, functions, and arguments
- Determines the value of
this
for the current context
Please note: the function is not yet executed.
This is the representation of an execution context:
fnExecutionContext ={
scopeChain:{// Current scope + scopes of all its parents},
variableObject:{// All the variables including inner variables & functions, function arguments},this:{}}
Activation Phase
The function is executed in this phase and the variables defined in variableObject
are initialized here.
Let’s understand this with the help of a simple example:
functionfoo(a, b){var c = a + b
functionbar(){return c
}returnbar()}foo(1,2)
When the function foo
is invoked, the interpreter first creates its execution context as below:
fooExecutionContext ={
scopeChain:{},
variableObject:{},this:{}}
The function foo
now enters the creation phase. In this phase, the interpreter creates the scope chain, scans the code without executing it and declares all its variables in the variableObject
, and evaluates the value of this
. Let’s see the state of execution context after this phase:
fooExecutionContext ={
scopeChain:{/* scope of function foo + global scope */},
variableObject:{
arguments:{0:1,1:2,
length:2},
a:1,
b:2,
c: undefined,
bar:'pointer to function bar()'},this:{}}
Please note: the arguments passed to the function foo
are evaluated in the creation phase. The local variable c
is declared on variableObject
and is initialized with a value of undefined
.
The evaluation of this
is not in the scope of this article. I recommend reading Let’s get this this
once and for all to have a clear understanding of this
in JavaScript.
Let’s now see the state of the execution context when the function enters into the activation phase:
fooExecutionContext ={
scopeChain:{/* scope of function foo + global scope */},
variableObject:{
arguments:{0:1,1:2,
length:2},
a:1,
b:2,
c:3,
bar:'pointer to function bar()'},this:{}}
Notice the value of the variable c
. The interpreter evaluates the value of c
when it comes across this line var c = a + b
. When it comes across the invocation for function bar
, it first creates the execution context for bar
and then the function bar
gets into the creation phase where the interpreter creates its scope chain, variableObject
and evaluates the value of this
. In the case of function bar
, its scope chain will have its own context as well the context of the function foo
, and hence the value of c
is not undefined
inside the function bar
.
Inferences from our Understanding of the Two Phases
Making Sense of the Arguments Array
When I first learned about the arguments
array, I thought of it as some magical data structure that just pops up on its own.
functionadd(){var a = arguments[0]var b = arguments[1]return a + b
}add(1,2)
Where is the arguments
array defined and how does the function add
get the reference to it? I have not even declared the arguments
array, but my code is running perfectly fine!
After learning about the execution of JavaScript, I understood that the arguments
array is not magic but an array created by the interpreter during the creation phase. Sigh!
A Word on Hoisting
A strict definition of hoisting suggests that variable and function declarations are physically moved to the top of your code, but this is not in fact what happens. Instead, the variable and function declarations are put into memory during the compile phase, but stay exactly where you typed them in your code. - MDN
Hmm. Let’s try to understand the above statement:
The interpreter scans the entire function code and puts the variable and function declarations in the variableObject
of the execution context during the creation phase. The declarations are not moved anywhere; they are just added in the variableObject
and kept aside in the memory.
We’ve learned many things before now, like execution context, scopes and scope chains, closures, and hoisting.
Cool, but what about CallStack, MemoryHeap, Callback Queue and Event Loop?
It’s not just the JavaScript engine that is responsible for the execution of JavaScript in the browser. The CallStack, MemoryHeap, Callback Queue and Event Loop play a significant role here! Let’s get to them one by one:
CallStack and MemoryHeap
CallStack is a Stack data structure that stores the JavaScript statements sequentially for execution. It helps the interpreter in knowing what is to be executed next!
Let’s say we have a function foo
as below:
functionadd(a, b){var c = a + b;return c;}functionfoo(){var a =1;var b =2;var c =add(a, b)
console.log('value of a', a);
console.log('value of b', b);
console.log('value of c', c);}foo()
The callstack for the above code looks like this:
Notice how statements are pushed onto the stack and then executed one by one by the interpreter.
MemoryHeap is used for storing the variables and functions.
What Is an Asynchronous Code and How Is It Executed?
As we can see from the above diagram on callstack, the statements are executed instantly and they don’t block the stack. However, there are pieces of code that take some time to execute. For example, the network calls take a while to serve a response over the wires.
Does that mean the JavaScript code responsible for a network call would sit on the stack until it finishes execution and gets something back from the server? This would mean that the callstack cannot accept any event in this period of time. The entire screen will look unresponsive! Clearly, we have to think of some better alternatives to handling code that takes time to execute.
The network calls, timers, and other bits of code that take time to execute are put in a separate thread without blocking the main callstack. They execute asynchronously in another thread, and, once their execution is complete, someone gives a shout out to the CallStack to take it forward!
Here’s an illustration on how CallStack, MemoryHeap, Callback Queue and Event Loop work together to execute JavaScript code:
Let’s understand this with an example:
functionfoo(){var a ='Hello World'setTimeout(functionbar(){
console.log('Inside function bar')},1000)}
The first statement in the above code takes almost no time to execute. The second setTimeout
statement takes a function bar
as the first argument, and this function bar
should be executed after 1000ms
as stated in its second argument.
setTimeout
won’t sit in the CallStack for 1000ms
and block everything. Instead, it would be pushed to another thread for 1000ms
. After 1000ms
, the function bar
will be pushed to the Callback Queue (Another data structure that handles the callbacks for asynchronous pieces).
Event Loop
Someone gives a shout out to the CallStack that the asynchronous code is done executing: “You please handle the rest!” That someone is Event Loop.
The Event Loop sits between the CallStack and the Callback Queue. Whenever it sees that the CallStack is empty, it simply puts stuff from Callback Queue to the CallStack for execution. At this moment, the function bar
would be executed by the CallStack.
Please note: the CallStack may become empty after 1000ms
and not exactly at 1000ms
. Hence the setTimeout
and setInterval
calls guarantee that the callbacks will be executed any time but no sooner than the interval.
And that’s how the asynchronous code is executed in the browser!
Conclusion
This is the final part of the three part series on The Journey of JavaScript: From Downloading Scripts to Execution. We started from how scripts are downloaded and then parsed by the JavaScript engine. We saw how parsers generate the scopes and abstract syntax tree. We learned all about the Ignition and Turbofan inside the V8 JavaScript engine and also how Arrays and Object models are handled by the engine.
In this part, we saw how JavaScript code is actually executed by the browser. We learned all about Execution Context and scopes and how closures and hoisting are supported by the JavaScript engine. We also learned about the CallStacks, MemoryHeap, Callback Queue and Event Loop.
This post has been brought to you by Kendo UI
Want to learn more about creating great web apps? It all starts out with Kendo UI - the complete UI component library that allows you to quickly build high-quality, responsive apps. It includes everything you need, from grids and charts to dropdowns and gauges.