Last Minute Notes

Advanced JavaScript

Patterns, pitfalls, and deeper JavaScript concepts.

📘 JavaScript Advanced Interview Notes

Deep Async, Event Loop, Tricky Outputs & Machine Coding

Table of Contents

Part 1: Deep Async JavaScript & Event Loop

  1. JavaScript Is Single-Threaded — Then How Is Async Possible?
  2. Runtime Architecture
  3. Event Loop Priority Order
  4. Microtasks vs Macrotasks
  5. Deep Event Loop Example
  6. Nested Microtasks
  7. async / await Internals
  8. await vs then
  9. Blocking the Event Loop

Part 2: Output-Based Tricky JavaScript Questions

  1. var + Closure
  2. let vs var Hoisting
  3. this Confusion
  4. Arrow Function this Trap
  5. Object Mutation
  6. Object Keys Stringification
  7. == Weirdness
  8. Function Hoisting Priority

Part 3: JavaScript Machine Coding Questions

  1. Debounce Function
  2. Throttle Function
  3. Polyfill for Promise.all
  4. Polyfill for bind
  5. Flatten Nested Array
  6. Deep Clone Object
  7. Once Function
  8. Implement Event Emitter
  9. Retry Promise Function
  10. Implement sleep

1. Deep Async JavaScript & Event Loop (INTERNALS)


1.1 JavaScript Is Single-Threaded — Then How Is Async Possible?

JavaScript has:

  • Single Call Stack
  • Asynchronous runtime environment

Async behavior is handled by:

  • Web APIs
  • Task queues
  • Event Loop

1.2 Runtime Architecture (Mental Diagram)


1.3 Event Loop Priority Order (CRITICAL)

Execution order:

  1. Call Stack
  2. Microtask Queue
  3. Macrotask Queue

Microtasks always execute before macrotasks

Flowchart: The Event Loop Cycle

Read it as a loop that never stops: script, then all microtasks, then one macrotask, then repeat.


1.4 Microtasks vs Macrotasks

Microtasks

  • Promise.then
  • Promise.catch
  • Promise.finally
  • queueMicrotask
  • MutationObserver

Macrotasks

  • setTimeout
  • setInterval
  • setImmediate (Node)
  • requestAnimationFrame

1.5 Deep Event Loop Example (Most Asked)

console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

Promise.resolve().then(() => {
  console.log("C");
});

console.log("D");

Execution Breakdown

  1. A → Call stack
  2. setTimeout → Web API
  3. Promise.then → Microtask queue
  4. D → Call stack empty
  5. Microtask → C
  6. Macrotask → B

Output

A
D
C
B

Sequence diagram: Why the Output Is A D C B

Follow the arrows top to bottom.


1.6 Nested Microtasks (Very Tricky)

Promise.resolve().then(() => {
  console.log("A");
  Promise.resolve().then(() => {
    console.log("B");
  });
});

setTimeout(() => {
  console.log("C");
}, 0);

Output

A
B
C

Reason:

  • Microtasks can schedule more microtasks
  • All microtasks must finish before macrotasks

1.7 async / await Internals (IMPORTANT)

async function test() {
  console.log("A");
  await Promise.resolve();
  console.log("B");
}

console.log("C");
test();
console.log("D");

How it works internally

  • await pauses function execution
  • Remaining code goes to microtask queue

Output

C
A
D
B

Sequence diagram: What await Does

await hands control back to the caller, and the rest of the function resumes later as a microtask.


1.8 await vs then (Same Priority)

await Promise.resolve();
Promise.resolve().then();

Both go to microtask queue.


1.9 Blocking the Event Loop (Interview Trap)

setTimeout(() => console.log("A"), 0);

for (let i = 0; i < 1e10; i++) {}

console.log("B");

Output

B
A

Why:

  • Call stack is blocked
  • Event loop cannot push tasks

Flowchart: Why a Blocked Stack Delays the Timer

The timer is ready after 0 ms, but nothing can run it until the call stack is free.


2. Output-Based Tricky JavaScript Questions


2.1 var + Closure (Classic)

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}

Output

3
3
3

Fix

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}

Flowchart: var vs let in a Loop

var gives every callback the same variable, let gives each iteration its own.


2.2 let vs var Hoisting

console.log(a);
var a = 10;

console.log(b);
let b = 20;

Output

undefined
ReferenceError

2.3 this Confusion

const obj = {
  name: "JS",
  getName: function () {
    return this.name;
  },
};

const fn = obj.getName;
console.log(fn());

Output

undefined

Reason:

  • Lost context
  • this → global / undefined (strict)

2.4 Arrow Function this Trap

const obj = {
  name: "JS",
  greet: () => {
    console.log(this.name);
  },
};

obj.greet();

Output

undefined

Arrow functions do not bind this


2.5 Object Mutation

const obj = { a: 1 };
const obj2 = obj;

obj2.a = 2;
console.log(obj.a);

Output

2

2.6 Object Keys Stringification

const obj = {};
obj[1] = "one";
obj["1"] = "one again";

console.log(obj);

Output

{ "1": "one again" }

2.7 == Weirdness

[] == ![];

Output

true

Explanation:

  • ![] → false
  • [] == false → "" == 0 → true

Flowchart: Why [] == ![] Is true

Each step is one coercion, applied left to right.


2.8 Function Hoisting Priority

foo();

var foo = function () {
  console.log("A");
};

function foo() {
  console.log("B");
}

Output

B

Function declarations beat var


3. JavaScript Machine Coding Questions (INTERVIEW LEVEL)


3.1 Debounce Function

function debounce(fn, delay) {
  let timer;

  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

Use cases:

  • Search input
  • Resize events

3.2 Throttle Function

function throttle(fn, limit) {
  let lastCall = 0;

  return function (...args) {
    const now = Date.now();
    if (now - lastCall >= limit) {
      lastCall = now;
      fn.apply(this, args);
    }
  };
}

Use cases:

  • Scroll events
  • Infinite scroll

3.3 Polyfill for Promise.all

Promise.myAll = function (promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let completed = 0;

    promises.forEach((p, i) => {
      Promise.resolve(p)
        .then((res) => {
          results[i] = res;
          completed++;
          if (completed === promises.length) {
            resolve(results);
          }
        })
        .catch(reject);
    });
  });
};

Flowchart: Promise.all Polyfill

Results are stored by index so the output order matches the input order, not the completion order.


3.4 Polyfill for bind

Function.prototype.myBind = function (context, ...args) {
  const fn = this;
  return function (...innerArgs) {
    return fn.apply(context, [...args, ...innerArgs]);
  };
};

3.5 Flatten Nested Array

function flatten(arr) {
  return arr.reduce((acc, val) => {
    if (Array.isArray(val)) {
      acc.push(...flatten(val));
    } else {
      acc.push(val);
    }
    return acc;
  }, []);
}

3.6 Deep Clone Object

function deepClone(obj) {
  if (obj === null || typeof obj !== "object") return obj;

  const copy = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    copy[key] = deepClone(obj[key]);
  }
  return copy;
}

3.7 Once Function

function once(fn) {
  let called = false;
  let result;

  return function (...args) {
    if (!called) {
      called = true;
      result = fn.apply(this, args);
    }
    return result;
  };
}

Flowchart: once

The result is cached in the closure, so later calls return it without running fn again.


3.8 Implement Event Emitter

class EventEmitter {
  constructor() {
    this.events = {};
  }

  on(event, fn) {
    this.events[event] = this.events[event] || [];
    this.events[event].push(fn);
  }

  emit(event, data) {
    (this.events[event] || []).forEach((fn) => fn(data));
  }

  off(event, fn) {
    this.events[event] = (this.events[event] || []).filter((f) => f !== fn);
  }
}

3.9 Retry Promise Function

function retry(fn, retries) {
  return fn().catch((err) => {
    if (retries === 0) throw err;
    return retry(fn, retries - 1);
  });
}

Flowchart: Retry

Each failure calls retry again with one fewer attempt, until the attempts run out.


3.10 Implement sleep

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

On this page