How Javascript Works Behind The Scenes
How Javascript Works Behind The Scenes
Now that we have a basic understanding of the JS language we will take this to the next level and look
behind the scenes at how JS works. This advanced understanding of how the language functions will
allow us to write better code, debug our code and understand other people’s code easier. The
purpose of this section is to make us better developers. This is a mix of theory and coding examples.
Already downloaded.
JavaScript is always hosted in some environment - most typically a browser. This is where JavaScript
runs. There can also be other hosts such as the NoJS web server or some applications that accept JS
code input. We will always focus on the browser in this course.
When we write JS code and actually want to run it, there is a lot happening behind the scenes. The
host has some sort of JS engine that takes our code and executes it. In simple terms, a JS engine is a
program that executes JS code. There are many different engines out there, like Google’s V8 engine
used in Google Chrome, SpiderMonkey, JS core and many more.
The first thing that happens inside the engine is that our code is parsed by a parser which reads our
code line by line and checks that the syntax used in our code is correct. Hence the parser understands
how JS code should be written and checks if it is valid. If we’ve made some mistakes the parser flags
an error and stops execution.
If everything is correct, then the parser produces a data structure known as an Abstract Syntax Tree,
and then converts our code from JS code into Machine Code - roughly a set of instructions which can
be executed directly by the computer processor.
It’s only once our code is converted to machine code that it actually runs and does its work.
OUR CODE -> PARSER --- (ABSTRACT SYNTAX TREE) ---> CONVERSION TO MACHINE CODE ---
(MACHINE CODE) ---> CODE RUNS
All JS code needs to run in an environment and these environments are called execution contexts. An
execution context is like a box/container which stores variables and in which a piece of our code is
evaluated and executed. The default execution context is always the global context.
In the global execution context, all of the code that is not inside any function is executed. This is
associated with the global object - which in the case of the browser, is the window object. Thus
everything we declare in the global execution context is essentially a part of the window object. In
this context, a variable lastName and window.lastName are the same thing, i.e. lastName becomes
like a property of the window object, which makes sense as properties are just variables attached to
objects.
For code in functions: Each time we call a function, it gets its own brand new execution context.
Our variable assignments take place in the global execution context. Our function declarations are
also stored/executed in our global context. However, when we call the function, we create a new
context for the function which is placed “on top of our current context”, forming the EXECUTION
STACK. Once we call the function, we switch to the Execution Context for the function and all
variables within the function are stored/assigned within the Execution Context rather than the Global
Context. Multiple functions leads to multiple Execution Contexts in our Execution Stack. Once the
function has completed, we say the function “returns” and the Execution Context leaves the top of
the Execution Stack.
38. Execution Contexts in Detail: Creation and Execution Phases and Hoisting.
We can associate an execution context with an object - the Execution Context Object. This object has
three properties:
Variable Object (VO), which will contain function arguments, inner variable declarations and function
declarations.
Scope chain, which contains the current variable object as well as the variable objects of all its
parents.
“This” variable, which we’ve already seen in action.
When a function is called, a new execution context is put on top of the execution stack. This happens
in two phases - the creation phase and the execution phase.
1. Creation phase.
A) Creation of the Variable Object (VO).
B) Creation of the scope chain.
C) Determine value of the “this” variable.
2. Execution phase.
The code of the function that generated the current execution context is ran line by line and all the
variables are defined. If it’s a global context, then it’s a global code that is executed.
These last two points are what we refer to as “hoisting”: functions and variables are hoisted in JS,
which means that they are available before the execution phase actually starts. They are hoisted in a
different way though - functions are already defined before the execution phase starts, while
variables are set to undefined and will only be defined in the execution phase.
So each execution context has an object which stores a lot of important data that the function will use
while it’s running, and this happens before the code is even executed.
calculateAge(1990);
function calculateAge(year) {
console.log(2016 - year);
}
Code like this still functions, even though we’ve been writing it alternatively throughout the course.
This is an example of hoisting. In the creation phase of the execution context, which is - in this case,
the global execution context, the function declaration calculateAge is stored in the variable object
before the code is executed.
Then when we enter the execution phase, the calculateAge function is already available for us to use.
retirement(1990);
var retirement = function(year) {
console.log(65 - (2016 - year));
}
Functions written as function expressions (like this) rather than function declarations, do not obey the
same rules. This code does not work. We can not hoist function expressions.
console.log(age);
var age = 23;
console.log(age);
When we’re using variables like this, our first result comes back as “undefined”. This is because in the
creation of the VO the code is scanned for variable declarations and the variables are then hoisted
and set to undefined. This is because in the creation phase we create the variable, but do not yet
assign it a value until the execution phase.
var age = 23;
console.log(age);
function foo() {
console.log(age);
var age = 65;
console.log(age);
}
foo();
console.log(age);
Consider this code: our outputs read 23, undefined, 65, 23.
We first log the age variable which is assigned to the VO in the global execution context object, 23.
Then we call the “foo function”, which first logs “undefined” as the age variable is hoisted in the foo
function execution context but not yet assigned. We then log 65 as we’re printing the age variable
from the VO of the foo function execution context object. Finally, after our function is run, we log 23
again, as we’re printing age within the global execution context and the value of age is different in the
VO of the global execution context and the VO of the foo function execution context.
The biggest takeaway from hoisting, however, is not to do with variables, but is that we can use
function declarations before we’ve actually declared them in our code. This is very useful and can be
confusing for new devs.
40. Scoping and the Scope Chain
The creation of the variable objects and function hoisting are the first step of the creation phase. Now
let’s talk about the second step - the creation of the scoping chain.
Scoping basically answers the question “where can we access a certain variable or function?”
In JS, each function creates a scope - this is the space or environment in which the variables that it
defines are accessible. In other programming languages, scopes can also be created by if/why/for
blocks, but that is not the case in JS. Here in JS, the only way to create a new scope is to write a new
function - this is very important in JS.
JS uses lexical scoping - this means that a function that is lexically within another function (I.e. written
inside another function) gets access to the scope of the outer function/parent function, and with that
it gets access to the variables and functions that the parent function defines.
var a = 'Hello!';
first();
function first() {
var b = 'Hi!';
second();
function second() {
var c = 'Hey!';
console.log(a + b + c);
}
}
The whole code sits within the global scope and thus has access to the global VO. The code within the
“first” function sits in the “first() scope” and has access to the variables and functions defined in the
global VO and VO1 associated with the execution context of the “first” function. Similarly, the
function “second” sits in the “second() scope” and has access to the functions and variables from VO2
(associated with the execution context of the “second” function, VO1 and the global VO) due to its
lexical positioning within the context of “first” and the global context. This is why it can read the
variables a and b despite their definitions being outside of its context.
So the global scope contains a, the “first() scope” contains a and b, and the “second() scope” contains
a, b and c. This is what we refer to as the scope chain. When JS does not find a variable within its
immediate execution context, it moves up the scope chain in search of the variable. We only return an
error when JS does not find a variable anywhere.
Note that this does not work backwards. The global scope will not have access to the variables b or
c unless they are returned from their functions. Locally scoped variables are not visible to their
parent scopes. The scope chain only works “upwards”.
How this works is that in the creation phase of each execution context object, the object gets the
exact scope chain, which is basically all of the variable objects that the execution context has access
to.
In our above example, in the second scope we have access to the variable objects of the second
function, the first function and the global variable object.
Here is a visual example of the difference between the execution stack and the scope chain. Note that
we indent scopes to show which are contained in which “levels”. The third() scope here is contained
within the global scope but since third() is not contained lexically within second() or first() then it does
not have access to the scope of these functions - and thus doesn’t contain its variables, even though
the third() function is called within the second() function.
Execution contexts store the scope chain of each function in the variable object, but they do not have
an effect on the scope chain itself. These are inherently different concepts.
Note that the second() function can still call the third() function because it has access to the global
scope, even thought the third() function can not read the variables b and c which are defined outside
of its scope.
The third and final step of the creation phase is determining and setting the value of the “this”
variable/keyword. Each and every execution context gets the “this” variable and it is stored in the
execution context object. But where does the “this” variable point?
In a regular function call: the this keyword points at the global object. In a browser for example, this is
the window object. This is the default case for the “this” keyword.
In a method call: the “this” variable points to the object that is calling the method. (Recall that a
method is a function which is attached to an object.)
The “this” keyword is not assigned a value until a function where it is defined is actually called.
Thus even though it appears that the “this” variable refers to the object where it is defined, the “this”
variable is technically only assigned a value as soon as an object calls a method. This is because the
“this” variable is attached to an execution context, which is only created once a function is
called/invoked.
console.log(this);
This line of code, when executed, returns “Window” in our console log and details the properties of
the window object.
calculateAge(1985);
function calculateAge(year) {
console.log(2016 - year);
console.log(this)
}
This code returns a value for age as predicted, and the “this” function once again points to the
Window object. This makes sense because this is a regular function call in the global context and
hence the object that this function is attached to is the global object.
In a regular function call, the “this” keyword always points to the window object.
var john = {
name: "John",
yearOfBirth: 1990,
calculateAge: function() {
console.log(this);
}
}
john.calculateAge();
var john = {
name: "John",
yearOfBirth: 1990,
calculateAge: function() {
console.log(this);
console.log(2016 - this.yearOfBirth);
function innerFunction() {
console.log(this);
}
innerFunction();
}
}
john.calculateAge();
With this code, we get our john object as the output from calculateAge, the age returned as expected
from calculateAge, and then innerFunction returns the window object when logging this. This is a
controversial topic in the JS community. Some people think innerFunction should return the john
object still. The argument for this happening is that it follows the rule - when a regular function call
occurs, then we return the global object - the window object, in this case. This is because
innerFunction, despite being written inside a method, is not a method itself, it’s a regular function.
var mike = {
name: "Mike",
yearOfBirth: 1984,
};
mike.calculateAge = john.calculateAge;
mike.calculateAge();
In the following example, we are “borrowing a method” from the john object to use in the mike
object. The output we receive here is the age calculated for mike, and then the this keyword points to
the mike object when we call mike.calculateAge(). This is an example of how the this object is only
assigned a value when the object calls a method. If this was not the case, then mike.calculateAge
would still return the john object from the this keyword as the this keyword is only actually
typed/used in the john object. However, the this keyword has no value and is only assigned an object
when the method is called, i.e. when we run mike.calculateAge.