Behind the scenes of JavaScript

Behind the scenes of JavaScript

An overview of how JavaScript works under the hood.

JavaScript is a high-level, prototype-based object-oriented, multi-paradigm, interpreted or just-in-time compiled, dynamic, single-threaded , garbage-collected programming language with first-class functions and a non-blocking event loop concurrency model.

Let us learn about this definition in detail.

1. High Level:

  • Every program that runs on the computer needs some hardware resources such as the memory and the CPU to do its work.

  • Now there are low-level languages such as C, where you have to manually manage these resources. For example, asking the computer for memory to create a new variable.

  • On the other hand, we have high-level languages such as JavaScript and Python where we do not have to manage resources at all because these languages are so-called abstractions that take all of that work away from us.

  • This makes the language easy to learn and to use, but the downside is that programs will never be as fast or as optimized as for example C programs.

2. Garbage collection:

  • This is one of the most powerful tools that takes memory management away from developers.

  • It is basically an algorithm inside the JavaScript engine, which automatically removes old, unused objects from the computer memory in order not to clog it up with unnecessary stuff.

3. Interpreted or just-in-time compiled:

  • The computer processor only understands zeros and ones.

  • Ultimately, every single program needs to be written in zeros and ones which is also called machine code.

  • And since that's not really practical to write, we simply write human-readable JavaScript code, which is an abstraction over machine code but this code eventually needs to be translated to machine code.

  • And that step can be either compiling or interpreting.

  • This step is necessary in every single programming language because no one writes machine code manually.

  • In the case of JS, this happens inside the JS engine.

4. Multi-paradigm:

  • In programming language, a program is an approach and an overall mindset of structuring our code, which will ultimately direct the coding style and technique in a project that uses a certain paradigm.

  • The three proper paradigms are- procedural paradigm, object-oriented programming (OOP), and functional programming(FP).

5. Prototyped-based object-oriented language:

  • Almost everything in JS is an object, except for primitive values such as numbers, strings, etc.

  • Why do we create an array and then use the push method on it? For example- const arr= [1,2,3]; arr.push(4); const hasZero = arr.indexOf(0) >-1;

  • Basically, we create arrays from an array blueprint, which is like a template and this is called the prototype.

Untitled-2022-09-20-1023.png

  • This prototype contains all the array methods and the arrays that we create in our code then inherit the methods from the blueprint so that we can use them on the arrays.

6. First-class functions:

  • JS is a language in which functions are simply treated as variables. We can pass them into other functions and return them from other functions. Example- const closeModal= function(){ modal.classList.add(''hidden''); overlay.classList.add(''hidden''); } overlay.addEventListener(''click''.closeModal);

7. Dynamic:

  • This means JS is a dynamically typed language.

Untitled-2022-09-20-1054.png

8. Single-threaded and Non-blocking event loop:

  • Concurrency Model- how the JavaScript engine handles multiple tasks happening at the same time.

    Untitled-2022-09-20-1107.png

  • JS runs in one single thread, so it can only do one thing at a time. (A thread is like a set of instructions that is executed in the computer's CPU.)

    Untitled-2022-09-20-1107.png

  • Sounds like it would block the single thread. However, we want non-blocking behavior.

    Untitled-2022-09-20-1107.png

  • By using an event loop which takes long running tasks to the event queue.

The JS Engine and Runtime

JS Engine: it is a program that executes JavaScript code. Every browser has its own JS engine but the most famous one is Google's V-Eight.

  • All the JS engine always contains a call stack and a heap.

  • Call-stack is where our code is actually executed using something called execution contexts.

  • Heap is an unstructured memory pool that stores all the objects that our application needs.

Untitled-2022-09-20-1136.png

Working of JS Engine

  • As a piece of JS code enters the engine, the first step that takes place is parsing which means to read the code.

  • During the parsing process the code is parsed into a data structure called the Abstract Syntax Tree (AST).

  • This works by first splitting up each line of the code into pieces that are meaningful to the language like the const or function keywords and then saving all these pieces into the tree in a structured way.

  • This step also checks if there are any syntax errors and the resulting tree will later be used to generate the machine code.

  • The next is a compilation, which takes the generated AST and compiles it into machine code.

  • This machine code then gets executed right away because remember modern javascript engines use just-in-time compilation and execution happens in the JS engine call stack.

  • Modern JS engines actually have some pretty clever optimization strategies. What they do is create a very unoptimized version of machine code in the beginning just so that it can start executing as fast as possible.

  • Then in the background, this code is being optimized and recompiled during the already running program execution.

  • This can be done most of the time and after each optimization, the unoptimized code is simply swept for the new more optimized code without stopping execution. And this process is what makes modern engines such as the V-eight so fast.

  • All this parsing, compilation, and optimization happens in some special threads inside the engine that we cannot access from our code. So, it is completely separate from the main thread that is basically running into a call stack executing our own code.

Untitled-2022-09-21-0927.png

JavaScript runtime in the browser

  • We can imagine a JavaScript runtime as a big box or a big container that includes all the things that we need in order to use JavaScript (in this case, it's the Browser).

  • And the heart of any JavaScript runtime is always a JavaScript engine. Without an engine, there is no runtime and there is no JavaScript at all.

  • However, the engine alone is not enough in order to work properly, we also need access to web APIs.

  • Web APIs are functionalities provided to the engine, but which are actually not part of the JavaScript language itself.

  • JavaScript simply gets access to these APIs through the global window object.

  • A typical JavaScript runtime also includes a so-called callback queue. This is a data structure that contains all the callback functions that are ready to be executed. For example, we attach event handler functions to DOM elements like a button to react to certain events.

  • The first thing that actually happens after the event is that the callback function is put into the callback queue. Then when the callback queue is empty, the callback function is passed to the stack, so that it can be executed and this happens by something called the event loop.

  • So basically, the event loop takes callback functions from the callback queue and puts them in the call stack, so that they can be executed.

What is an Execution context?

  • Suppose that our code was just finished compiling. So the code is now ready to be executed.

  • What happens now is that a so-called global execution context is created for the top-level code. Top-level code is basically the code that is not inside any function.

  • So in the beginning the code outside the function will be executed.

  • An execution context is an abstract concept. It is basically defined as an environment in which a piece of JavaScript code is executed. It's like a box that scores all the necessary information for some code to be executed. Such as local variables or arguments passed into a function.

  • Now, we have the environment, so the top-level code can be executed (it's just the computer's CPU processing the machine code that is received).

  • Once the top-level code is executed, functions finally start to execute as well. For each and every function call, a new execution context is created containing all the information that is necessary to run exactly that function. All these execution contexts together make up the call stack.

  • Now, when all the functions are done executing, the engine will basically keep waiting for callback functions to arrive so that they can be executed. For example, a callback function is associated with a click event.

What is inside the execution context?

  1. Variable environment- In this environment all our variables and function declarations are stored and there is also a special arguments object. This object contains all the arguments that were passed into the functions that the current execution context belongs to.

  2. Scope chain- It basically consists of references to variables that are located outside of the current function.

  3. This keyword- Each context also gets a special variable called the this keyword.

Now the content of the execution context (variable environment, scope chain, and this keyword) is generated in a so-called creation phase which happens right before execution.

Note: Execution contexts belonging to arrow functions do not get their own arguments keyword nor do they get this keyword. Instead, they can use the arguments object and this keyword from their closest regular function parent.

What is the call stack?

  • Its basically a place where execution context gets stacked on top of each other in order to keep track of where we are in the program execution.

  • So, the execution context that is on the top of the stack is the one that is currently running and when it's finished running it will be removed from the stack and execution will go back to the previous execution context.

Untitled-2022-09-21-1227.png

Scoping and scope in JavaScript

Scope: It is a space or environment in which a certain variable is declared (variable environment in case of functions).

Scope concept: It controls how our program's variables are organized and accessed by the JavaScript engine.

Now, in JS we have something called lexical scoping, which means that the way variables are organized and accessed. It is entirely controlled by the placement of functions and of blocks in the program's code. For example, a function that is written inside another function has access to the variables of the parent function.

NOTE: variable scoping is influenced by where exactly we write our functions and code blocks.

Scope of variable- it is the region of our code where a certain variable can be accessed.

THE THREE DIFFERENT TYPES OF SCOPE IN JS-

Untitled-2022-09-21-1255.png

Scope Chain

Untitled-2022-09-21-1754.png

  • When the variables are not found in the child scope (current) they look up for it in the parent scope.

  • But a parent scope will never look for variables in the child scope (does not have access to it.

*NOTE: In ES6 not only functions but also blocks create scopes. For a variable declared with var, block scopes don't apply. Var is function scoped. Let/const are block scoped. *

Scoping in nut-shell-

  • Scoping asks the question "Where do variables live?" or "Where can we access a certain variable and where not?"

  • There are 3 types of scope in JS: the global scope, the function scope, and the block scope.

  • Only let and const variables are block-scoped. Variables declared with var end up in the closest function scope.

  • In JS, we have lexical scoping, so the rules of where we can access variables are based on exactly where in the code functions and blocks are written.

  • Every scope always has access to all the variables from all its outer scopes. This is the scope chain.

  • When a variable is not in the current scope, the engine looks up in the scope chain until it finds the variables it's looking for. This is called variable look-up.

  • The scope chain is a one-way street: a scope will never, ever have access to the variables of an inner scope.

  • The scope chain in a certain scope is equal to adding together all the variables and environments of all parent scopes.

  • The scope chain has nothing to do with the order in which functions were called. It does not affect the scope chain at all.

Hoisting in JavaScript

  • Hoisting makes some types of variables accessible/usable in the code before they are actually declared. (Variables lifted to the top of their scope)

    Untitled-2022-09-21-2135.png

  • Before execution, the code is scanned for variable declarations, and for each variable a new property is created in the variable environment object.

  • Hoisting does not work the same for all variable types. So let's analyse how hoisting works-

Untitled-2022-09-21-2140.png

  1. Function declarations are actually hoisted and the initial value in the variable environment is set to the actual function. What this means is that we can use function declarations before they are actually declared in the code because they are stored in the variable environment object even before the code starts executing.

  2. Variables declared with var are also hoisted, but hoisting works in different ways here. So, unlike functions, when we try to access a var variable before it's declared in a code we don't get the declared value but we get undefined.

  3. Let and const variables are not hoisted, technically they are actually hoisted but their value is basically set to uninitialized. So there is no value to work with at all. And so in practice, it is as if hoisting was not happening at all. Instead, we say, that these variables are placed in a so-called Temporal Dead Zone (TDZ) which makes it so that we can't access variables between the beginning of the scope and to place where the variables are declared.

  4. Depends if they are created using var/let or const. Because keep in mind that these functions are simply variables and so behave the exact same way as variables in regard to hoisting. This means that a function expression or arrow function created with var is hoisted to undefined, but if created with let or const it's not usable before it's declared in a code because of TDZ.

Temporal Dead Zone (TDZ)

Untitled-2022-09-21-2234.png

  • In this example code, we're gonna look at the job variable.

  • It is a const, so it's scoped only to this if block and it will be accessible starting from the line where its defined because there is a Temporal Dead Zone for the job variable. It is basically the region of the scope in which the variable is defined but can't be used in any way, so it is as if the variable did not even exist.

  • Now if we still try to access the variable while in the TDZ like we actually do in the first line of this if block, then we get a reference error telling us that we can't access the job before initialization.

  • However, if we try to access a variable that was actually never even created, like here in the last line of this if block, then we get a different message saying that (x) is not defined.

  • What this means is that the job is in fact in the TDZ where it is still initialized but the engine knows that it will eventually be initialized because it already read the code before and set the job variable, in the variable environment to unitialised. Then when the execution reaches the line where the variable is declared, it is removed from the TDZ and its then safe to use.

The this keyword

  • The this keyword or this variable is basically a special variable that is created for every execution context and therefore any function.

  • In fact, we learned before that it's one of the three components of any execution context along with the variable environment and scope chain.

  • In general terms, the 'this keyword' is not static. It depends on how the function is called and its value is only assigned when the function is actually called.

  • Let us analyze the different ways in which a function can be called-

  1. The first way to call a function is as a method. So, as a function attached to an object. When we call a method, the this keyword inside that method will simply point to the object on which the method is called or in other words, it points to the object that is calling the method.

  2. The other way is to simply call a function, not as a method and so not attached to any object. In this case, the this keyword will simply be undefined (only valid for strict mode).

  3. Next, we have arrow functions, and while arrow functions are not exactly a way of calling functions, it's an important kind of function that we need to consider because remember, arrow functions do not get their own this keyword. Instead, if we use 'this keyword' in an arrow function it will simply be 'this keyword' of the surrounding functions and so of the parent functions. In technical terms, this is called the lexical this keyword.

  4. Lastly, if the this keyword is called as an event listener, then this keyword will always point to the DOM elements that the handler function is attached to.

NOTE: This keyword does not point to the function itself and also not to the variable environment of the function.

Summary

I hope you find this blog helpful in understanding the workings of JavaScript behind the scenes.