Runtime Engine, Callstack, Scope

JIT compilers

In modern browsers and in other environments such as NodeJS, JavaScript is a Just In Time Compiled language, which is a mix between Ahead Of Time Compilation and Interpretation.

After parsing and analyzing the source code, this approach achieves both interpretation followed by execution the explicit source code, and generating an optimized low-level language (Bytecode) which can be later executed if a similar context appears in the original source code thread.

V8 JS engine compiling example

A process called Hoisting is also performed before execution. All declarations, for both variables and functions, are processed first during the compilation phase before any of the code is executed. The declaration code is moved, or hoisted, from where they appear in the flow of the code to the top of the scope and pushed to the heap. Only declarations are hoisted, while assignments or other executable logic is left in place. Functions declarations are hoisted first, then variable declarations.

Therefore a function can be called even if it is explicitly described further in the code and variables declared with var do not throw errors if referenced before being assigned any value.

loggToConsole() //it will print Hello
x //it will print undefined

function loggToConsole() {
  console.log('Hello')
}

var x = 5

JavaScript runtimes

Popular JS runtimes (engine and additional methods) are V8 (behind Chrome, recently Microsoft Edge and NodeJS), JavaScriptCore (Safari) and SpiderMonkey (Mozilla Firefox).

The JS engine is single-threaded indeed but inside a runtime, we can achieve asynchronous computing. The anatomy of the runtime is described below.

The Call Stack operates as a Last-In-First-Out (LIFO) structure. When a function is called, it’s pushed onto the stack; once it completes, it's popped off. This process repeats until all functions are executed.

Once a task requires external APIs or methods (such as DOM manipulation, setting a timeout, or making an external API request), it triggers an asynchronous operation. When the operation completes, its callback is pushed to the Callback Queue. These callbacks will be executed in the order they were added, but only after the Call Stack is completely cleared of all synchronous tasks.

//Execution context
debugger //it allows step by step execution and evaluation
console.log('1')
setTimeout(function() {
	console.log('2')
}, 3000)
console.log('3')
console.log('4')
console.log('5')
setTimeout(function() {
	console.log('6')
}, 0)
console.log('7')
console.log('8')
console.log('9')

The microtask queue is a special queue that is processed by the JavaScript engine before the callback queue. This means that microtasks, such as promises or mutation observers, are always executed before any tasks from the callback queue, even if the callback was scheduled first.

console.log("Start");

// Callback queue (setTimeout)
setTimeout(() => {
  console.log("Timeout callback");
}, 0);

// Microtask queue (Promise)
Promise.resolve().then(() => {
  console.log("Promise microtask");
});

console.log("End");

// OUTPUT:

// Start
// End
// Promise microtask
// Timeout callback

Even though the setTimeout callback is initiated before the Promise, the promise is placed in the microtask queue and runs before the timeout, which is in the callback queue.

// here is an example of how HTTP fetches are added to the microtask queue

fetch('https://example.com/api/users')
  .then(response => response.json())
  .then(data => {
    // This callback will be executed after the HTTP fetch is complete
  });

HTTP response promises are added to the microtask queue with the following aims:

  • It ensures that HTTP fetches are executed as soon as possible.

  • It prevents HTTP fetches from blocking the main thread.

  • It allows developers to write more efficient and responsive asynchronous code.

Scope

The Scope is the context in which values and expressions are "visible" or can be referenced. If a variable or other expression is not "in the current scope," then it is unavailable for use. Scopes can also be layered in a hierarchy, so that child scopes have access to parent scopes, but not vice versa.

var x = "declared outside function";

exampleFunction();

function exampleFunction() {
    console.log("Inside function");
    console.log(x);
}
Example of the scope chain
//Scoping
var favouriteFood = "grapes";

var foodThoughts = function () {
     console.log("Original favourite food: " + favouriteFood);

     var favouriteFood = "sushi";

     console.log("New favourite food: " + favouriteFood);
};

foodThoughts()

Function parameters have local scope within that function's body.

Declaring variables with let turns the scope to block scope, meaning it basically creates a clone (a mask) if that variable has already been declared in the parent scope. More details on the ES6 features.

Last updated

Was this helpful?