Bent and hammered nail with an emoji face illustration. Done by Andre Coetzer
Section 8

How JavaScript works behind the scenes

Lecture 89

An High-Level Overview of JavaScript

Let's take a look at the saying " JavaScript is a High-Level, Object-Orientated, Multi-Paradigm programming language " and then expand on that to create the monster version.

Monster Version

JavaScript is a High-Level, Prototype-Based, Object-Orientated, Multi-Paradigm, interpreted or just-in-time compiled, Dynamic, Single Threaded, Garbage-Collection programming language, with first class functions and a Non blocking event loop concurrency model.

Let's unpack each of these and see what they mean.

High-Level

Any Computer program needs resources such as ram, CPU and so on, though with low level programming languages such as C, you have to manually manage these resources. Asking the computer for memory to create a new variable.

On the other side, you have high level programming languages like JavaScript and Python where the developers don't need to worry about this, as it happens automatically with something known as garbage collection.

Garbage-Collection

One of the powerful tools that takes away memory management is garbage collection. Garbage collection is an algorithm inside JavaScript which removes old, unused variables from memory to not clog up the memory.

Interpreted or just-in-time compiled language

Machine code is in 0's and 1's, though JavaScript is written in human-readable code which needs to be compiled to machine code, with JavaScript this happens within the JavaScript Engine.

Multi-Paradigm

In programming, a paradigm is an approach and mindset of structuring code, which will direct your coding style and technique.

There are 3 popular paradigms, which are, Procedural Programming, Object-Oriented Programming (OOP) and Functional Programming (FP).Procedural programming is what we have been doing up until now. That is programming in a very linear way with some functions in-between.

The thing to note about paradigms is that they can be imperative vs. declarative. More on this later in the course.Many languages are only procedural, OOP or FP, though with JavaScript it can do all of it.

Prototype-Based Object-Oriented

The OOP nature of JavaScript is a prototype base object-oriented approach. This means that almost everything in JavaScript is an object, except for primitive values.

Earlier we said Arrays are objects, and why is it that we can use a push method on an array, when we now know that methods are related to objects? This is due to prototypal inheritance, an array is created from an array blueprint called the prototype.

This prototype contains all the array methods. The arrays we create in our code inherit the methods from the blueprint. Later, we will discuss OOP in more detail.

First class functions

This means that functions are treated the same as variables. We can pass them into other functions and even return functions. This allows for functional programming.

                    
                      overlay.addEventListener('click', closeModal);
                    
                  

Above is an example of functional programming we used recently, whereby the event handlers took in a named function.

Dynamic

JavaScript is a dynamic typed language, meaning when we assign a value to a variable without having to define the type. In other words, types become known at runtime.

The value can also change from one type to another, though if you're interested in a strong typed JavaScript, then you can check out TypeScript.

Single-Threaded Non-Blocking Concurrency Model

This will be discussed later in the course as well, due to it being such a complex topic. So concurrency model means how the JavaScript engine handles multiple tasks happening at the same time.

We need this because JavaScript runs in a single thread, so it can only do one thing at a time. So if there was a long-running task like fetching data from the backend, this could block the single thread, which is known as block behavior. We want non-blocking behavior.

So by JavaScript using event loops, that takes long-running tasks and executes them in the background and puts them back in the main thread once they are finished.

Lecture 90

The JavaScript Engine and Runtime

A JavaScript engine is simply a program that executes JavaScript code. Every browser has its own JavaScript engine, the most well known one is the V8 engine from Google, the V8 engine powers Google Chrome and Node.js.

The JavaScript engine, Call Stack and Heap

Any JavaScript engine contains a Call Stack and a Heap. The call stack is where our code is executed using something known as execution context. The heap is an unstructured memory pool where all objects, or reference types, are stored. More on primitive and reference types later.

But how is the code compiled to machine code, so that it can actually be executed afterward? Before we can answer that question, we need to understand the difference between compilation and interpretation.

Compiling to machine code can happen either through compilation or interpretation.

Compilation vs Interpretation vs Just-In-Time

Interpretation is where the interpreter runs through the source code line by line. This used to be how JavaScript work, though today this is no longer acceptable. So now it has what is known as just-in-time, which is a mix between compilation and interpretation.

When speaking about the JavaScript engine, and when our code enters the engine. The first step is to parse the code. During the parsing phase, the code is passed to the AST (Abstract Syntax Tree).

The next step is to take the AST and compile it to machine code. This machine code gets executed right away as it uses just-in-time compilation, The execution happens in the Call Stack.

Now that the program is running it doesn't stop there, the code, even though it's running, goes through some optimization strategies, where the code becomes more and more optimized, this allowing it to execute the program as fast as possible and over time optimize the code and replace the current code.

JavaScript Runtime

Next, we take a look at the JavaScript Runtime. The runtime is like a container that includes all the things needed to use JavaScript, (In this case in the browser).

At the heart of a runtime is a JavaScript engine, though this is not enough, we also need things like the Web APIs and Callback Queues. When an event happens, it is placed into the callback queue. When the call stack is empty, it's then passed to the engine and executed.

This process happens by something called the event loop. This is basically an overview of the Non Concurrency Model. A runtime can also exist outside the browser, such as Node.js, the only thing missing is the Web APIs. Just remember, there are other runtimes available apart from browsers.

Lecture 91

Execution Contexts and The Call Stack

What is the execution context within the call stack? It executes code starting with global execution, (Top Level Code). An example of this is code that is outside a function. So, an execution context is basically an environment in which a piece of JavaScript code is executed.

You get one global execution context, this is all your top level code. Then you get another for the first function and then the second and so forth.

                    
                      const myName = 'Andre';

                      const first = function () {
                        let a = 1;
                        const b = second(7, 8);
                        a = a + b;
                        return a;
                      };

                      const second = (numOne, numTwo) => {
                        var c = numOne + numTwo;
                        return c;
                      };

                      const x = first();
                    
                  

The call stack ensures that the order of execution never gets lost. The program will stay in the global state until it is closed, by closing the tab or browser.

Lecture 92

Scope and The Scope Chain

Each execution context has a variable environment, a scope chain and a 'this' keyword. Scoping is how our variables are organized and accessed. Scoping asks where do variables live and or where can we access a certain variable and where can we not.

In JavaScript, we have what is known as lexical scoping, which is scoping that is controlled by the placement of functions and blocks in the code. An example of this is a function written inside a function. That inner function has access to the parent variables, but not the parent to child.

Scope means the space or environment in which a certain variable is declared.

                    
                      // Global Scope
                      const me = 'Andre';
                      const job = 'Developer';
                      const birthYear = 1989;
                      const isAlive = true;

                      // Function Scope

                      const calcAge = function (birthYear) {
                        const now = 2037;
                        const age = now - birthYear;
                        return age;
                      };

                      //Block Scope
                      if (!isAlive) {
                        const output = 'Sorry, but you are dead';
                        console.log(output);
                      }
                    
                  

There is a global scope, function scope and a block scope. Scopes can only look up in a scope chain, but never into a scope.Important to remember is that let and const are block scoped, but var is not.

In Summary

Scoping asks where do variables live, or where we can access them. There are 3 types of scopes in JavaScript, Global, function and block. let and const are blocked scoped, while var is not. JavaScript uses lexical scoping.

Every scope has access to all outer or parent variables. This is known as the scope chain, but never the other way around.

Lecture 93

Scoping in Practice

                    
                      const firstName = 'Andre';

                      function calcAge(birthyear) {
                        const age = 2037 - birthYear;
                        function printAge() {
                          const output = `You are ${age}, born in ${birthYear} and your name is ${firstName}`;
                          console.log(output);
                        }
                        printAge();
                      }

                      calcAge(1989);
                    
                  
Lecture 94

Variable Environment: Hoisting and The TDZ

Hoisting makes some types of variables accessible / usable in the code before they are actually declared. So basically hoisting list variables to the top of their scope. An example of hoisting is function declarations.

Var is also hoisted, but remains undefined, while let and const are not hoisted and are placed in the TDZ or temporal dead zone. With function expressions and arrow functions, it all depends with what it was created, so depending if its a let, const or var.

The TDZ or Temporal Dead Zone, is basically the area from the start of the execution to when the variable is declared so if you call on a variable before it has been declared, you will full into the TDZ.

Lecture 95

Hoisting and TDZ in Practice

                    
                      if (!numProducts) deleteShoppingCart()

                      var numProducts = 10;

                      function deleteShoppingCart () {
                        console.log('Your shopping cart was deleted');
                      };
                    
                  

Above is a perfect example of why never to use var, because var gets hoisted to undefined the if statement will execute before that because undefined is a falsey value. As a result, it will delete your shopping cart.

Some good practice to avoid things like this is to always write your variables on top of each scope, always declare your functions first and call them after, and never use var!

Lecture 96

The this Keyword

The 'this' keyword is part of the execution context. The 'this' keyword points to the owner function. The 'this' keyword in a method points to the object.

If 'this' was used Globally, and if using strict mode would return undefined, though in sloppy mode, returns window. With arrow functions it does not get its own 'this' keyword but points to the parents 'this' keyword.

With event listeners, the 'this' keyword inside a function will point to the DOM element the handler is attached to. The 'this' keyword does not point to the function itself, and also not its variable environment.

Lecture 97

The this Keyword in Practice

                    
                      console.log(this);

                      const calcAge = function (birthYear) {
                        console.log(2037 - birthYear);
                        console.log(this);
                      };

                      const calcAge2 = birthYear => {
                        console.log(2037 - birthYear);
                        console.log(this);
                      };

                      const myself = {
                        firstName: 'Andre',
                        lastName: 'Coetzer',
                        birthYear: 1989,
                        calcAge: function () {
                          console.log(2037 - this.birthYear);
                          console.log(this);
                        },
                      };
                    
                  
Lecture 98

Regular Functions vs. Arrow Functions

                    
                      const andre = {
                        year: 1989,
                        firstName: 'Andre',
                        calcAge: function () {
                        console.log(this);
                        console.log(2037 - this.year)
                        },
                        greet: () => {
                        console.log(`Hello, ${this.firstName}`)
                        }
                      }

                      // access to arguments
                      function sum (a, b) {
                        console.log(arguments)
                        return a + b;
                      }
                    
                  

In the above example, the arrow functions 'this' keyword will point towards the parent, which in this case is the window object and since we should always be using strict mode, it should return undefined.

Never use arrow functions in a method.

Functions also have access to a keyword called 'arguments'.

Lecture 99

Primitives vs. Objects (Primitive vs. Reference Types)

                    
                      let age = 30;
                      let oldAge = age;
                      age = 31;

                      console.log(oldAge);
                      console.log(age);

                      const me = {
                        name: 'Andre',
                        age: 30,
                      }

                      const friend = me;
                      friend.age = 27;
                      console.log(`Friend: ${friend.age}`);
                      console.log(`Me: ${me.age}`);
                    
                  

The above example happens of something known as primitive types and reference types in memory. Primitive types are stored in the call stack, while object or reference types as they are known are stored in the heap.

JavaScript Runtime

In the diagram above, it's important to note that the identifier pints to the address and not the value itself. This diagram also explains why with const we can change the object value, whereas with primitives we cannot.

Whenever you think you copy the object, you're in fact just creating a new variable pointing to the same object reference.

Lecture 100

Primitives vs. Objects in Practice

                    
                      // Primitive types
                      let lastName = 'Williams';
                      let oldLastName = 'lastName';
                      lastName = 'Davis';
                      console.log(`last name: ${lastName}, old last name: ${oldLastName}`);

                      // Reference types
                      const jessica = {
                        firstName: 'Jessica',
                        lastName: 'Williams',
                        age: 27,
                        family: [
                          'Brody', 
                          'Alice'
                        ]
                      };

                      const marriedJessica = jessica;
                      marriedJessica.lastName = 'Davis';

                      console.log('Jessica:', jessica);
                      console.log('Married Jessica:', marriedJessica);

                      // Coping Objects
                      const jessicaCopy = Object.assign({}, jessica);
                      jessicaCopy.lastName = 'Coetzer';

                      // altering info on a second level with object.assign()
                      jessicaCopy.family.push('Margarita')
                      jessicaCopy.family.push('Alfred')

                      console.log('Jessica Copy:', jessicaCopy);
                    
                  

As we have seen, primitives change as follows, and they point to the address rather than the value. Though, to actually copy the object, we can use the 'object.assign({}, the object you want to copy)' function to make a new object where the properties were copied.

There is a problem with using this function however and that is that it only works for the top level, if there is an object inside the object, then this inner object will still point to the same place in memory.

So 'object.assign({}, object)' only creates what is known as a shallow copy and not a deep copy. To create a deep copy, we will need to use an external library such as 'low dash' that houses tools in order to achieve this.