JavaScript_870x220

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 previous two articles of the series:

  1. The script tags are blocking in nature. When the main thread encounters a script 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.
  2. 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

  1. Execution Context
  2. Terminologies Related to Execution Context
  3. Scopes and Scope Chain
  4. Creation Phase
  5. Activation Phase
  6. Inferences from our Understanding of the Two Phases
  7. callstack and memoryheap
  8. What Is an Asynchronous Code and How Is It Executed
  9. 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!'

function helloWorld () {
    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.

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 = 1

function foo () {
    var b = a + 1

    function bar () {
        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 = {}

function baz () {
    obj.a = 'Hello'

    return function bar () {
        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 = {}

function baz () {
    obj.prototype.a = 'Hello'

    return function bar () {
        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:

  1. Creates the scope chain
  2. Creates the variables, functions, and arguments
  3. 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:

function foo (a, b) {
    var c = a + b

    function bar () {
        return c
    }

    return bar ()
}

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.

function add () {
    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:


function add (a, b) {
    var c = a + b;
    return c;
}

function foo () {
    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:

function foo () {
    var a = 'Hello World'
    setTimeout(function bar () {
        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.

KendoJSft


Ankita Masand
About the Author

Ankita Masand

A Software engineer trying to make sense of every line of code she writes, Ankita is a JavaScript enthusiast and adores its weird parts. She’s also an obsessed developer, highly focused on the performance of web applications. When she’s not writing JavaScript, you’ll find her engaged in understanding the intricacies of deep neural networks.

Related Posts

Comments

Comments are disabled in preview mode.