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. Here is the execution model:

┌─────────────────────────────────┐
│         Call Stack               │  ← Synchronous code executes here
│  (one function at a time)       │
└──────────┬──────────────────────┘
┌─────────────────────────────────┐
│       Microtask Queue            │  ← Promise callbacks, queueMicrotask()
│  (drains completely before       │     MutationObserver
│   moving to macrotasks)          │
└──────────┬──────────────────────┘
┌─────────────────────────────────┐
│       Macrotask Queue            │  ← setTimeout, setInterval,
│  (one task per loop iteration)   │     I/O callbacks, UI rendering
└─────────────────────────────────┘

Critical rule: The microtask queue is drained completely before the next macrotask runs.

// 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:

Rule When this is
1. new binding new Foo() The newly created object
2. Explicit binding call(), apply(), bind() The specified object
3. Implicit binding obj.method() The object before the dot
4. Default binding Plain function call undefined (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
Feature Map WeakMap
Key types Any Objects only
Enumerable Yes (forEach, keys()) No
Garbage collection Prevents GC of keys Allows GC of keys
Use case General-purpose Metadata, caching without leaks

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

Quiz

Module 2: JavaScript & TypeScript Mastery

Take Quiz