Skip to content
JavaScript interview 5 min read

Advanced JS Interview Questions

Advanced JavaScript interviews move past syntax and probe how the engine actually behaves: how closures capture state, how the prototype chain resolves lookups, and the exact order in which microtasks and macrotasks drain. These questions separate people who memorized definitions from people who understand the runtime. The set below covers closures, prototypes, async ordering, performance utilities, and the output-prediction puzzles that show up constantly.

Closures and scope

Why does a loop with var print the same value, and how do you fix it?

A closure captures variables by reference, not by value. With var, every iteration shares one function-scoped binding, so by the time the callbacks run the loop has finished and they all read the final value. let creates a fresh binding per iteration.

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

Output:

var: 3
var: 3
var: 3
let: 0
let: 1
let: 2

What is a common closure-based memory leak?

A closure keeps every variable in its enclosing scope alive as long as the closure is reachable. If a long-lived callback closes over a large object it never uses, the garbage collector cannot reclaim that object.

function attach(node) {
  const bigData = new Array(1_000_000).fill("x");
  node.addEventListener("click", () => console.log("clicked"));
  // bigData is captured by the closure's scope and never freed
}

Capture only what you need, or null out large references inside the closure once you are done with them.

Prototypes and inheritance

How does property lookup traverse the prototype chain?

When you read obj.prop, the engine checks obj itself, then Object.getPrototypeOf(obj), and so on until it hits null. Writes, however, normally happen directly on the object and shadow the prototype.

const animal = { speak() { return "..."; } };
const dog = Object.create(animal);
dog.speak = function () { return "woof"; };

console.log(dog.speak());                    // woof (own)
console.log(Object.getPrototypeOf(dog) === animal); // true
console.log("speak" in dog, dog.hasOwnProperty("speak")); // true true

What is the difference between __proto__ and prototype?

ConceptLives onPurpose
prototypeFunctions/classesThe object new instances will inherit from
__proto__Every object instanceA reference to the object’s actual prototype (legacy accessor)
Object.getPrototypeOf()Standard APIThe canonical way to read an object’s prototype
class User {}
const u = new User();
console.log(Object.getPrototypeOf(u) === User.prototype); // true

Async ordering

What is the difference between microtasks and macrotasks?

The event loop drains the entire microtask queue (promise callbacks, queueMicrotask, MutationObserver) after each macrotask (timers, I/O, setTimeout) and before rendering. So promises always resolve before the next timer fires.

console.log("1 sync");
setTimeout(() => console.log("2 timeout"), 0);
Promise.resolve().then(() => console.log("3 microtask"));
console.log("4 sync");

Output:

1 sync
4 sync
3 microtask
2 timeout

Predict the output: async/await with chained promises

await suspends the function and schedules its continuation as a microtask, so it interleaves with other resolved promises.

async function main() {
  console.log("A");
  await null;
  console.log("B");
}
main();
Promise.resolve().then(() => console.log("C"));
console.log("D");

Output:

A
D
B
C

A and D are synchronous; B (the continuation after await) and C are both microtasks queued in source order.

How does process.nextTick compare in Node?

In Node, the nextTick queue runs before the promise microtask queue, which runs before timers and setImmediate. Overusing nextTick can starve the event loop.

Performance utilities

Implement debounce

Debounce delays execution until activity stops — ideal for search-as-you-type. Only the last call within the window runs.

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

Implement throttle

Throttle guarantees a function runs at most once per interval — ideal for scroll and resize handlers.

function throttle(fn, limit) {
  let waiting = false;
  return function (...args) {
    if (waiting) return;
    fn.apply(this, args);
    waiting = true;
    setTimeout(() => (waiting = false), limit);
  };
}
TechniqueFiresUse case
DebounceAfter the pauseAutocomplete, form validation
ThrottleAt a steady rateScroll, drag, resize

Currying and functional patterns

What is currying and how do you implement it generically?

Currying transforms f(a, b, c) into f(a)(b)(c), collecting arguments until enough are supplied.

function curry(fn) {
  return function curried(...args) {
    return args.length >= fn.length
      ? fn.apply(this, args)
      : (...rest) => curried.apply(this, [...args, ...rest]);
  };
}

const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3), add(1, 2)(3), add(1, 2, 3)); // 6 6 6

Generators and iterators

How do generators enable lazy sequences?

A generator function pauses at each yield and resumes on next(), producing values on demand instead of all at once.

function* idGenerator() {
  let id = 1;
  while (true) yield id++;
}
const gen = idGenerator();
console.log(gen.next().value, gen.next().value); // 1 2

Generators also power two-way communication: gen.next(value) injects a value back as the result of the paused yield.

Tricky output prediction

Predict: this binding loss

Detaching a method drops its receiver; the lost this is a classic bug.

const obj = {
  name: "DevCraftly",
  greet() { return `Hi ${this.name}`; },
};
const fn = obj.greet;
console.log(obj.greet());        // Hi DevCraftly
console.log(fn.call(obj));       // Hi DevCraftly

Predict: coercion quirks

console.log([] + []);        // "" (empty string)
console.log([] + {});        // "[object Object]"
console.log(0.1 + 0.2 === 0.3); // false (floating point)
console.log(typeof NaN);     // "number"

Always compare floats with a tolerance: Math.abs(a - b) < Number.EPSILON.

Best Practices

  • Prefer let/const to avoid closure-capture surprises in loops.
  • Reach for Object.getPrototypeOf over the legacy __proto__ accessor.
  • Remember the rule: all microtasks drain before the next macrotask and before paint.
  • Debounce input-driven work and throttle high-frequency events to protect the main thread.
  • Null out large references captured by long-lived closures to prevent leaks.
  • Use generators for lazy, infinite, or pausable sequences instead of building huge arrays.
  • Never trust loose equality or float equality — use === and an epsilon comparison.
Last updated June 1, 2026
Was this helpful?