JavaScript & TypeScript Mastery

JavaScript Engine Internals

5 min read

Understanding how JavaScript executes code is the foundation of every frontend interview. These concepts appear in coding rounds, trivia questions, and debugging scenarios.

The Event Loop

JavaScript is single-threaded but non-blocking, thanks to the event loop. One turn of that loop looks like this:

One turn of the event loop

The ordering question in every phone screen comes from this cycle. Note where rendering sits: it is not a queued task competing with your callbacks, it is a phase the browser reaches once per turn — which is why a long task blocks paint as well as input.

stack emptiesif rendering this turncallbacks donenext turncallback pushedCall stackSynchronous code, one frame at …ONE PER TURNOne macrotasksetTimeout, setInterval, an I/O…DRAINS FULLYMicrotask checkpointPromise callbacks, queueMicrota…requestAnimationFrameRuns only when the browser has …MAY BE SKIPPEDStyle, layout, paintA rendering opportunity, not a …

Critical rule: the microtask queue is drained completely before the next macrotask runs — and it is drained again after each macrotask, not once per loop.

An infinite chain of microtasks therefore starves the page permanently: each one queues the next, the drain never finishes, and the browser never reaches its rendering phase. An infinite chain of setTimeout(fn, 0) calls does not, because each callback is a separate macrotask with a checkpoint between them. This distinction is the follow-up question when a candidate answers the ordering question correctly.

// Classic interview question: predict the output order
console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

console.log('4');

// Output: 1, 4, 3, 2
// Why: 1 and 4 are synchronous (call stack).
// Promise.then is a microtask (runs before setTimeout).
// setTimeout is a macrotask (runs last).

Closures

A closure is a function that retains access to its lexical scope even after the outer function has returned.

// Interview classic: the loop closure problem
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3 (var is function-scoped, not block-scoped)

// Fix 1: use let (block-scoped)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2

// Fix 2: IIFE closure
for (var i = 0; i < 3; i++) {
  ((j) => {
    setTimeout(() => console.log(j), 100);
  })(i);
}
// Output: 0, 1, 2

Practical closure pattern — memoization:

function memoize(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const expensiveCalc = memoize((n) => {
  console.log('Computing...');
  return n * n;
});

expensiveCalc(5); // Computing... 25
expensiveCalc(5); // 25 (cached, no log)

The this Keyword

JavaScript has four rules for this binding, applied in this priority order:

RuleWhenthis is
1. new bindingnew Foo()The newly created object
2. Explicit bindingcall(), apply(), bind()The specified object
3. Implicit bindingobj.method()The object before the dot
4. Default bindingPlain function callundefined (strict) or globalThis
const obj = {
  name: 'Alice',
  greet() {
    console.log(this.name);
  },
  greetLater() {
    // Arrow functions inherit `this` from enclosing scope
    setTimeout(() => console.log(this.name), 100);
  }
};

obj.greet();      // 'Alice' (implicit binding)
obj.greetLater(); // 'Alice' (arrow fn inherits `this`)

const fn = obj.greet;
fn();             // undefined (default binding, lost context)

Interview tip: Arrow functions do NOT have their own this. They inherit this from the enclosing lexical scope. This is why arrow functions are preferred in callbacks and event handlers inside class methods or React components.

Prototypal Inheritance

Every JavaScript object has an internal [[Prototype]] link. Property lookups follow the prototype chain:

const animal = {
  speak() {
    return `${this.name} makes a sound`;
  }
};

const dog = Object.create(animal);
dog.name = 'Rex';
dog.speak(); // 'Rex makes a sound' (found via prototype chain)

// ES6 class syntax is syntactic sugar over prototypes
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  bark() {
    return `${this.name} barks`;
  }
}

WeakMap and WeakSet

These are less commonly known but appear in senior interviews:

// WeakMap: keys must be objects, and are garbage-collected
// when no other references exist
const metadata = new WeakMap();

function process(obj) {
  if (!metadata.has(obj)) {
    metadata.set(obj, { processedAt: Date.now() });
  }
  return metadata.get(obj);
}

// Use case: attaching data to DOM elements without memory leaks
// When the element is removed from DOM, the WeakMap entry
// is automatically garbage-collected
FeatureMapWeakMap
Key typesAnyObjects only
EnumerableYes (forEach, keys())No
Garbage collectionPrevents GC of keysAllows GC of keys
Use caseGeneral-purposeMetadata, caching without leaks

Next, we'll cover TypeScript patterns that appear frequently in frontend interviews. :::

Quiz

Module 2: JavaScript & TypeScript Mastery

Take Quiz
Was this lesson helpful?

Sign in to rate