Javascript Parsers and Engines
Javascript is always hosted in some environment, typically it’s hosted in a browser that is Google Chrome, Firefox, Safari etc. Javascript will run in this environment but not restricted to only this it can also run in other hosts such Nodejs web server or some application that accepts JS code input.
I will only focus on the browser. When you write your Javascript and run it there is a lot of stuff happening behind the scenes. In the browser, we have a Javascript engine that takes the code and executes it. Each browser has its own javascript engine Google Chrome has a V8 engine, Firefox has Spider Monkey and Safari has a Nitro engine, so every browser will have a JS engine.
Inside the engine the first step is to parse the code by a parser, it will read the code line by line and will check that the syntax is correct. The parser will know rules for JS to ensure this. If it finds any mistakes it will throw an error and execution will stop.
If everything is correct the parser will produce a data structure which is called Abstract Syntax Tree and that is then turned into machine code, machine code is executed directly by the computer’s processor and that is where our code runs. They are subtle differences about in the steps but these are the main steps for all.
Execution Contexts and Execution Stack
In the above section I gave you a conceptual overview and now we will talk more about the order in which the code is run.
The execution context is the environment in which Javascript is executed. Think of execution contexts like a box which contains variables and our code.
Global Execution Context (GEC):
It’s the default context. Code that is not inside any function. GEC cannot be more than one. The value of this is undefined if the code is in strict mode else it will give us a window object if it is written in a global context.
All the code that is not inside any function is in GEC. So any variables or functions, not inside the function is in GEC. GEC is an object which in case of a browser it will be a window object, everything you declare in a global context will be attached to the window object.
Execution Stack:
If the code is not inside a function it’s in GEC but what if the code is inside function, the answer is very simple it gets it’s brand new execution context.
Execution Stack is a stack data structure in which the new execution context is pushed on top of Global Execution Context. If inside the new execution context(which is a function basically) a new function is called it will create a new execution context and that new execution context is pushed on top of the previous execution context.
So now that everything is pushed onto our Execution Stack when do the items in the Execution Stack pop. It pops whenever the topmost execution context(which is a function basically) finishes its execution.
Let me explain it with an example.
var a = 'apple' one() function one() { var b = 'ball' var c = a + b two() } function two() { var d = 'dog' var e = a + d }
At Line 1 a GEC is pushed on the stack. At Line 2 when function one is called it makes a new execution context and is pushed on top of the previous GEC on line 6 a new execution context is created and is pushed on top of the previous new context. After function two() finishes its execution. It pops out its execution context and after that function one() finishes it’s execution and pops its execution context.
Execution Contexts in Detail: Creation and Execution phases and Hoisting
In the above section, we talked about when the execution context is created and now we will talk about how it is created. An execution context can be considered as an object and this object has three properties.
Variable Object(VO):
It will contain function argument, function declaration, inner function declaration and inner variable or variable declaration. Function declaration, pointing to the function and variable declaration are commonly called Hoisting.
Scope Chain:
It contains its own variable objects as well as its parent’s variables object.
“this” variable:
Based on the scope chain the value of this is initialized. If this is a bit confusing don’t worry once I explain scope chain “this” will be a lot clearer.
When a function is called inside a function a new execution context is created for it and is pushed on top of the execution stack as discussed before this step happens in two phases the Creation phase and Execution phase.
All the three properties VO, Scope Chain and “determining the value of this” is done in the Creation phase. After that in the Execution phase, the code generated in the current execution context is run line by line.
Hoisting
Function and variables are available before the execution phase starts. Functions and variables are hoisted in a different way, functions are already defined before the execution phase starts while variables are set to be undefined and will only be defined in the execution phase.
Let’s look into hoisting by looking into practical examples.
testHoisting() function testHoisting() { console.log('function is hoisted') }
The above code will work, in the creation phase of the execution context in this its a Global Execution Context(GEC) the function declaration testHoisting is stored in a Variable Object (VO) before the code is executed, so when we enter the execution phase the function testHoisting is already available. So we can declare the function later and use it first in our code.
This only works for function declaration as mentioned before but not for variables, there is another thing called function expressions.
testHoisting() var testHoisting = function() { console.log('function is not hoisted') }
The above code will throw an error, because this is not a function declaration but a function expression and hoisting will only work with function declarations.
Above we worked on functions now lets try it out on variables.
console.log(a) var a = 23 console.log(a)
The above code will print undefined and 23. This happens because in the creation phase of the variable object the variables are scanned for variable declarations and are set to undefined.
console.log(a) // var a = 23
The above code will give us a ReferenceError of a is not defined, because we don’t have any definition javascript won’t know about the variable.
var a = 23
function something() {
var a = 62
console.log(a)
}
something()
The above code will print 62 because something function has its own execution context and if write console.log(a) above line 3 we will get undefined.
That will be all for hoisting and an advantage for hoisting is that we can use function declaration before we declare them in our code.
Scoping and Scope Chain
Scoping is where can the variable be accessed. Each function has its own scoping space where the variables are defined. Typically a scope is created by if for blocks but not in Javascript. New scope is created by only writing new functions. In Javascript we also have lexical scoping for example a function inside another function will access to its outer function.
var a = 'apple' one() function one() { var b = 'ball' two() function two() { var c = 'cat' console.log(a+b+c) } }
The above code will print appleballcat because of lexical scoping that is the inner function function has access to outer function but not the other way round.
one() function one() { var a = 'apple' function third() { var c = 'cat' console.log(b) } two() function two() { var b = 'ball' third() } }
The above code will throw an error of RefrenceError of b is undefined but wait why can we even call third function inside two function and the answer is scope chain the two function has access to variables and functions of one function. Another thing is why are we getting undefined error and the answer again is scope chain function three is not an inner function of function two so it can’t access variable b.
“this” variable
Each and every new execution context gets a this variable. In a regular function call “this” keyword points to window or browser object which is a global object. Important thing to remember is that the this keyword is not assigned a value until a function where a function where it is defined is called.
Lets look into some examples
console.log(this) function one() { console.log(this) } obj_a = { a: 'apple', one: function() { console.log(this) } } obj_a.one() one()
At Line 1 it will print a window object because the global execution context over here is the window object. At line 3 it will again print a window object because it’s a function call not a method call. At line 8 it will print obj_a instead of a window object it makes sense because it’s a method call.
obj_a = { a: 'apple', one: function() { console.log(this) two() function two() { console.log(this) } } } obj_a.one()
We have extended our obj_a and added another function inside one method now on line 4 it will print obj_a and line 7 it will print a window object. At line 7 the result is unexpected that is because the this keyword over here is back to being the window. This is a bit counter intuitive but it makes sense because it’s not a method but a regular function call.
Conclusion
Now you know how javascript works behind the scenes. You learned about how the Javascript Engine runs about how the Execution context is created its phases and details about its phases. If you have any queries or any mistakes that you think I have made would be more than happy if you could give suggestions to further improve it.
I would like to thank Jonas and his course on Udemy for teaching me so much and I definitely recommend his course to learn about Javascript in depth. I am also active on linkedIn if you want my help in something.