Exploring the Fundamentals of JavaScript Engine

Cosmin Mihalache
12 min readSep 12, 2022

Diving into the Core Principles of How JavaScript Engine Operates

JavaScript is a single-threaded language because while running code on a single thread, it can be really easy to implement as we don’t have to deal with the complicated scenarios that arise in the multi-threaded environment like deadlock. Since JavaScript is a single-threaded language, it is synchronous. By being single-threaded it has a single calling stack.

Babel is the compiler that takes ES6 code and translates it to ES5.

JS Runtime

Memory heap

A large region in the memory of the JS engine where data can be stocked.

Call Stack

This is where all your javascript code gets pushed and executed one by one as the interpreter reads your program, and gets popped out once the execution is done.

Event Loop

JavaScript has a runtime model based on an event loop, which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks.

Callback Queue

Here is where asynchronous codes get pushed to and wait to be pushed by the event loop in the call stack.

Web API⏩ async:

  • DOM
  • Fetch
  • setTimeout()

Context

Execution Context

For each function called it’s created an execution context. Every time we run code is run in the execution context.

Global Execution Context

The last execution context is the Global Execution Context. Global execution context is formed of Global Object (in the browser is window) and this keyword.

Javascript Engine in the browser creates Global Execution Context.

Lexical Environment

Execution context is telling which Lexical environment is currently running. Lexical scope determines our available variables depending on where we call our functions. First lexical environment is global environment.

Hoisting

During the compilation phase, a function declaration is going to the top of the global execution context. Javascript analyzes what function or variable are created and allocate memory for them. Variables are partially hoisted, and functions are fully hoisted. It looks for the “var” or “function” keyword. It won’t work for “let” or “const”.

Functions

Definition: a set of statements that performs a task or calculates a value, but for a procedure to qualify as a function, it should take some input and return an output where there is some obvious relationship between the input and the output. Parameters are function variables.

Functions expression ➡ var canada= function(){}
Function declaration (get hoisted ) ➡ function india(){}
Function execution ➡ india()

Function arguments are the real values passed to (and received by) the function

Function argument ➡ arguments keyword helps to get all arguments from a function. Should be used as followed console.log(Array.from(arguments)) 😀.

Pure function is a function that has no side effects, and if you give a input will return the same output. Side effects means if the function modifies anything outside the function e.g. axios calls, mutation in an array, console.logs(because it has an effect the document). Are easy to test and to compose, and predictable.

  • A function’s pure if it’s free from side effects and returns the same output, given the same input.
  • Side-effects include: mutating input, HTTP calls, writing to disk, and printing to the screen.

Perfect functions should :

  • do 1 task have a return statement
  • be pure, no shared state
  • immutable state(return a new copy of the global state)
  • composable, predictable.

Imperative vs Declarative

Imperative code code that tells the machine what to do and how to do it. Machine code is imperative. A for loop is more imperative.

Declarative what to do, and what should happen. Higher level language is more declarative, we don’t worry about memory allocation and so on.

Variable Environment / Variable context → context of each function

Scope

4 types of scope in JS: Block Scope, Global Scope, Function Scope, and Module Scope.

A variable declared outside a function has Global Scope.

Function Scope vs Block Scope

Function scope refers to scope that is accessible only in functions.

To use Block Scope ( inside if statement / for loops). Block scoping means declaring a variable inside curly brackets. — let — gives opportunity to have block scope.

Scope Chain — all functions have a global lexical environment, hence this allows us to access global variables. Lexical scope determines what variables are available. Functions that have other functions as return share variables of their parents.

Scope Chain

⚠ var — it available outside block scope (outside block meaning outside {})

Global Variables — the main issue with these variables is that someone on another component may overwrite these variables.

IIFE - immediately invoked function expression. Can be called immediately after. It is an option to use function scope.

(function (){})()

Event propagation is the mechanism by which events in the Document Object Model (DOM) are processed. The DOM represents a hierarchical tree-like structure of nodes, where an event that occurs on one node can propagate to its parent, grandparent, and other ancestor nodes.

In JavaScript, there are two main event propagation methods: bubbling and capturing.

Bubbling: In the bubbling model, an event starts at the most deeply nested element and propagates up through its ancestors. For example, if a user clicks on a button, the click event would first be captured by the button, then it would bubble up through its parent elements, until it reaches the document element.

Capturing: In the capturing model, an event starts at the outermost element and propagates down through its children. For example, if a user clicks on a button, the click event would first be captured by the document element, then it would propagate down through its child elements, until it reaches the button.

“this” keyword

thisis the object that the function is a property of.

In JavaScript, the this keyword refers to an object.

this has dynamic scope

It also depends on how the function is called.

  • Gives methods access to their objects
  • Execute the same code for multiple objects

In an object, if we have a function that has another function in it with the keyword this, if we call the second word this, it will not return the object but will return the window object. This can be solved with the arrow function or return function2.bind(this). This happens because of the lexical reading of the Javascript Engine.

In JavaScript, the call(), apply(), and bind() methods allow you to call a function with a specified this value and arguments.

  • The call() method allows you to call a function with a specified this value and arguments, provided as a comma-separated list.
  • The apply() method is similar to the call() method, but the arguments are passed as an array.
  • The bind() method returns a new function with a specified this value and arguments, partially or completely bound.

Context vs Scope

Scope pertains to the visibility of the variables, and context refers to the object within which a function is executed.

Types in JS:

Primitives (such as numbers, strings, and booleans) are stored directly in memory, whereas non-primitives (such as objects and arrays) are stored as references. Primitives are more memory efficient

Primitives are data that represent a single value in memory. Primitive data types are not mutable. To change the value of primitive data in JS we have to assign it to a new value in memory without altering the previous one.

  • string
  • number
  • booleans
  • undefined -the absence of a definition
  • null — the absence of value
  • Symbol.

⚠Exception typeof undefined = Object

Non-Primitive are objects. An object holds a reference/address of a single key-value pair or many key-value pairs. When we refer to an object we refer to an address in memory with key and value pair.

  • objects
  • arrays
  • functions

Arrays and functions are also objects.

Immutability not changing the data or the state. We do this by copying the data or the state and modify the copy.

Built-in objects in JS :

  • null
  • undefined
  • infinity / Math
  • String
  • Number
  • Boolean

Objects are passed by reference.

A way to change object2 without changing obj1 is with spread operators. Spread operators work only for the first layer of the object. For other layers is better to use JSON or deep cloning.

How to compare objects?

function shallowEqual(object1, object2) {
const keys1 = Object.keys(object1);
const keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of keys1) {
if (object1[key] !== object2[key]) {
return false;
}}
return true;}

Type Coercion means JS transforms a type into another type. Type coercion happens when you use in JS “==” . This also happens if ( 1) = true / if (0) = false.

JavaScript is a dynamically typed language, but TypeScript is a statically typed language.

“Dynamic typing” means that the type of a value is determined at runtime, while “static typing” means that the type of a value is determined at compile time.

Function vs objects Function are callable objects.

Define function & Invoke

function abc (){} & abc() ➡abc.call()
const obj={ two :function (){return 2 }} ➡obj.two()
const four = new Function ('return 4') ➡ four()

High order function :

  • function that accepts another function as a parameter
  • functions that return another function
  • Clone Objects

You can safely clone, then mutate, your input. Just leave the original one untouched.

  • Spread syntax ( syntax) is the easiest way to shallowly clone objects.
  • JSON.parse(JSON.stringify(object)) is the easiest way to deeply clone objects.

Closures

Closures are features of JS that take care that function has access to variables outside of it. A closure is a combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

Advantages of Closures are memory efficiency and encapsulation.

Encapsulation — hiding information that has not to be seen outside. A function declared in another function is not directly exposed to the global scope.

Memory efficiency — by keeping values in memory, closures can be more memory efficient than using global variables or passing values as arguments between functions, because you don’t need to continually allocate and deallocate memory for these values.

Closures are also used in JavaScript callbacks and event handlers, where they provide a way to remember and access variables from the parent scope even after the event has completed.

function createCounter() {
let count = 0;
return function() {
return count++;
}
}

const counter = createCounter();
console.log(counter()); // 0
console.log(counter()); // 1
console.log(counter()); // 2

const closures = function (){
let count =0
return function increment (){
count ++
return count
}
}

const incrementFn=closures()
console.log(incrementFn()) // return 1
console.log(incrementFn()) // return 2

Prototypes

In JavaScript, prototypal inheritance is a mechanism for creating new objects that inherit properties and methods from existing objects. This is done using the prototype property of an object, which points to another object from which the properties and methods are inherited.

The Object.create() method is used to create a new object with a specific prototype, and the Object.getPrototypeOf() method is used to get the prototype of an object.

Here are some examples of how prototypal inheritance can be used in JavaScript:

  1. Creating a new object that inherits from an existing object:
const parent = {
name: 'John',
age: 30,
sayHello: function() {
console.log('Hello, my name is ' + this.name);
}
}

const child = Object.create(parent);
console.log(child.name); // "John"
console.log(child.age); // 30
child.sayHello(); // "Hello, my name is John"

2. Adding new properties and methods to an object

const parent = {
name: 'John',
age: 30,
sayHello: function() {
console.log('Hello, my name is ' + this.name);
}
}
const child = Object.create(parent);
child.gender = 'female';
child.sayGender = function(){console.log('I am '+ this.gender)}
console.log(child.gender); // "female"
child.sayGender(); // "I am female"

In order to see what properties are inherited can be used

obj.hasOwnProperty(property)
class Character {
constructor(name,weapon){
this.name=name;
this.weapon=weapon}
attack(){
return 'attack with' + this.weapon;
}
}
class Elf extends Character{ // extends the prototype to point to character
constructor (name,weapon,type){ // constructor gets run when is instaciated a new elf
super(name,weapon); // extends super class to Character
this.type=house}
}
const dolby = new Elf('Doby','cloth', 'house') // use new to create new instances

When using extends in a class we need to use super.

console.log(dolby instance Elf) // true
console.log(dolby instance Character) // true

Dolby is an instance of a class.

Extends inherit something of a class its properties.

Private and Public in OOP.

class Elf extends Character{ // extends the prototype to point to character
#age=54 // with hashtag we can add private state
constructor (name,weapon,type){ // constructor gets run when is instaciated a new elf
super(name,weapon); // extends super class to Character
this.type=house}
}

Promises

A promise is an object that may produce a single value sometimes in a future. Either a resolve value or a reject value. A promise can have three status: pending, fulfilled, or rejected.

Promises serve same things as callbacks.

const promise = new Promise((resolve,rejects)=>{
if(true){
resolve('Stuff Work')}
else{
reject('Error, its broke')}
})

promise
.then(result=> result + '!')
.then(result2=>{
// throw Error
console.log(result2)}
.catch(()=>console.log(error))

Catch only runs if any error is ‘throw’.

Promise.all([promise1, promise2, promise3])
.then(values=>{
console.log(values})

// Will return and array of responses
// If any of promise fails all of them will fail, will go in a catch

fetch return a promise that is pending. Promises can succeed or fail once.

ES8 → async/await

The goal using async await is to make code more readable and synchronous. Async await are promises under the hood, is syntactic sugar for promises.

Await has to be used in front of any function that returns a promise.

In order to catch errors instead of then catch in async await need to use try catch.

async function getData() {
const promise1 = fetch('https://jsonplaceholder.typicode.com/posts/1').then(response => response.json());
const promise2 = fetch('https://jsonplaceholder.typicode.com/posts/2').then(response => response.json());
const promise3 = fetch('https://jsonplaceholder.typicode.com/posts/3').then(response => response.json());

const results = await Promise.all([promise1, promise2, promise3]);
console.log(results);
}
getData();

Spread operators in ES9 ES2018 to review

Job queue — or microtask que is another queue similar with callback queu e that has priority over

Axios vs Fetch

Axios has a simpler syntax compared to the Fetch API, especially when it comes to error handling and sending data in the request body. Axios automatically serializes request data as JSON, and it provides a more convenient way to handle errors.

Axios provides a built-in way to cancel a request, which can be useful in certain scenarios. With the Fetch API, you need to implement cancellation yourself by using AbortController.

GET request does not have body.

//Fetch
fetch('https://example.com/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer my-secret-token'
},
body: JSON.stringify({
key1: 'value1',
key2: 'value2'
})
})
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});
// Axios
axios.post('https://example.com/api/data', {
key1: 'value1',
key2: 'value2',
}, {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer my-secret-token'
}
})
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error(error);
});

Paralel or sqeuencial or race in async code

Race > Paralel > Sequence

The Promise.race() static method takes an iterable of promises as input and returns a single Promise. This returned promise settles with the eventual state of the first promise that settles.

Paralel use Promise.all. use Promise.allSettled in order not to get error if one of the promise fails.

Sequence when using await for each function hence inside of async code is syncron.

Browser creates a new thread per tab.

Error Handling

Types of error:

  • new Error
  • new SyntaxError
  • new ReferenceError
//Error is a constructor function
new Error('oopsie')

throw new Error(); // Error gets thrown and stops the program,
// throw is used to display error

Error has three properties :

  • error.name
  • error.message
  • error.stack — to get the stack trace

Catch is important in order the program is not stop running.

  • try {} catch{}
  • catch()
async function (){
try{
await(axios.post....)

}
catch(err){
console.log(err.message)
}

}

Functional Programming

Functional programming separates data in functions

  • clear to understand
  • easy to extend
  • easy to understand
  • easy to maintain
  • memory efficient

Compose is the idea that any sort of data transformation should be obvious. A method to compose multiple function together. Is a system design. Pipe can change the order on how function are called.

const compose =(f,g)=>data=>f(g(data))

const multiplyBy3=(num)=>num*3
const makePositive=(num)=>Math.abs(num)

const multiplyBy3AndAbsolute=compose(mutiplyBy3,makePositive)

multiplyBy3AndAbsolute(-50) // return 150

Memorization — Catching a method to store values in order to use them later on in order to speed up applications.

Modules

Highly self contained, and group together, with they re own functionality, built to be move around and work.

Happy Coding! 💻

--

--

Cosmin Mihalache

Front end developer based in Romania, specialized in ReactJS, focused on developing clean, maintainable, and scalable web applications.