Runtime Engine, Callstack, Scope

Runtime Engine, Call Stack, Scope

JIT compilers

Modern JS engines (V8, JavaScriptCore, SpiderMonkey) parse your source into an AST, generate bytecode for a fast interpreter, then—based on runtime profiling—optimize hot paths with a JIT compiler. This mixed strategy balances quick startup with high peak performance.

V8 JS engine compiling example

Hoisting (what actually happens)

Before executing your code, the engine creates execution contexts and their environment records. During this compile/setup phase, declarations are registered:

  • Function declarations are hoisted with their full definition and can be called earlier in the file.

  • var declarations are hoisted and initialized to undefined (the assignment happens later).

  • let / const declarations are hoisted too, but remain in the Temporal Dead Zone (TDZ) until their declaration line runs—accessing them earlier throws a ReferenceError.

  • Function expressions are not hoisted as callable functions; only the variable binding (e.g., with var) is hoisted.

logToConsole();            // ✅ "Hello"
console.log(x);            // ✅ undefined (var is hoisted, assigned later)

function logToConsole() {
  console.log("Hello");
}

var x = 5;

Common fix: don’t rely on hoisting for readability—declare before use.


JavaScript runtimes (quick refresher)

A runtime = engine + Web/Host APIs + queues + event loop. The engine is single-threaded, but the runtime exposes asynchronous primitives (timers, network, DOM events) that schedule work back to JS.


Execution context & the Call Stack

  • The Call Stack is LIFO: a function call pushes a frame; returning pops it.

  • “Blocking” synchronous code runs to completion before the runtime can process queued callbacks/microtasks.

// Inspect order with DevTools
debugger; // step to observe the stack frames

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");

Microtasks vs (macro)tasks

  • Microtask queue runs after the current stack frame, before any timer/IO callbacks: Promise reactions, MutationObserver, queueMicrotask.

  • Task/callback queue contains timers, IO, UI events, etc. Result: Promise handlers run before setTimeout(fn, 0) callbacks.

console.log("Start");

setTimeout(function () {
  console.log("Timeout callback");
}, 0);

Promise.resolve().then(function () {
  console.log("Promise microtask");
});

console.log("End");

// Output:
// Start
// End
// Promise microtask
// Timeout callback

Note: fetch() completes in the host environment and resolves a Promise; its .then/.catch handlers run as microtasks.


Scope & the Scope Chain

Scope is where identifiers are visible. Child scopes can read parent bindings (via the scope chain), not vice-versa. JavaScript uses lexical (static) scoping: visibility is determined by where code is written.

var x = "declared outside function";

exampleFunction();

function exampleFunction() {
  console.log("Inside function");
  console.log(x); // reads from outer scope
}
Example of the scope chain

Hoisting meets scope

This classic example shows shadowing + var hoisting inside a function scope: the inner var favouriteFood is hoisted (set to undefined) and shadows the outer one until assignment.

var favouriteFood = "grapes";

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

foodThoughts();

// Output:
// Original favourite food: undefined   <-- inner var is hoisted but uninitialized
// New favourite food: sushi

Additional notes:

  • Function parameters are local to the function body.

  • Block scope (let/const) confines bindings to { ... } blocks and prevents use before initialization (TDZ). Prefer const, use let only when you must reassign; avoid var in modern code.


Quick takeaways

  • Don’t depend on hoisting for clarity—declare before use.

  • Understand the event loop order: stack → microtasks → tasks.

  • Scope is lexical; inner scopes shadow outer bindings. Keep variable lifetimes tight (block scope) to reduce bugs.

Last updated

Was this helpful?