Interview prep · Junior → Senior

JavaScript Interview Questions

The questions a JavaScript interviewer actually asks about the language and the browser — types and coercion, scope and closures, this, prototypes, promises and the event loop, iteration, the DOM and its event model, modules, and the classic gotchas — each answered with a short example and a link to the source. I keep these to the language, its runtime, and the core browser model, not framework trivia or whiteboard puzzles. Pair it with the JavaScript version reference and the open JavaScript roles on the jobs board.

Difficulty

Junior — expected of any JavaScript developer; syntax and fundamentals.
Mid — idioms, the runtime model, and the “why,” not just the “what.”
Senior — the event loop, async internals, performance, and trade-offs.
Difficulty

Showing 0 of 0 questions

1 · Core language & types

What are the primitive types in JavaScript, and how do they differ from objects? Junior

There are seven primitives: string, number, boolean, null, undefined, symbol, and bigint. Everything else — arrays, functions, dates, plain objects — is an object. Primitives are immutable and compared/copied by value; objects are mutable and handled by reference (the variable holds a reference, so two names can point at the same object).

let a = 10, b = a;   // copy of the value
b = 20;               // a is still 10

const o = { n: 1 }, p = o;  // both reference the same object
p.n = 99;             // o.n is now 99 too

Why it's asked / follow-up: value-vs-reference is the root of a whole class of “why did my object change?” bugs. Follow-up: “how do you copy an object so the original is untouched?” — a shallow copy ({...o} / Object.assign) or structuredClone(o) for a deep one.

Source: MDN — JavaScript data types and data structures.

What does typeof return, and what are its quirks? Junior

typeof returns a lowercase string naming the operand's type. The two quirks every interviewer wants you to know: typeof null is "object" (a historical bug, kept for compatibility), and typeof a function is "function" even though functions are objects. It's also safe on undeclared identifiers — it returns "undefined" instead of throwing.

typeof 42          // "number"
typeof "hi"        // "string"
typeof undefined   // "undefined"
typeof null        // "object"  ← the famous bug
typeof (() => {})   // "function"
typeof []          // "object"  → use Array.isArray() for arrays

Why it's asked / follow-up: it surfaces the typeof null trap and the limits of typeof for distinguishing object kinds. Follow-up: “how do you check for an array?” — Array.isArray(x), never typeof.

Source: MDN — typeof.

What is the difference between == and ===? Junior

=== (strict equality) compares value and type with no conversion. == (loose equality) performs type coercion first, applying a set of rules most people don't fully memorize — which is exactly why the convention is to always use ===. The one common exception is x == null, which neatly matches both null and undefined.

0 == ""        // true   — both coerce to 0 / falsy
0 == "0"       // true
"" == "0"      // false  — no longer numeric coercion
null == undefined  // true
0 === ""       // false  — strict: different types

Why it's asked / follow-up: coercion bugs are common and the inconsistency above (0=="0" but ""!="0") shows why. Follow-up: “when is == acceptable?” — the == null idiom; otherwise prefer ===.

Source: MDN — Equality comparisons and sameness.

What is the difference between null and undefined? Junior

undefined means “no value has been assigned” — it's what the engine gives an uninitialized variable, a missing argument, or a missing property. null is an intentional “no value,” set by your code. Both are falsy; the ?? nullish-coalescing and ?. optional-chaining operators treat the two together.

let x;              // undefined (uninitialized)
const y = null;     // deliberately empty
({}).missing        // undefined (absent property)
x ?? "default"     // "default" — ?? covers null AND undefined

Why it's asked / follow-up: it checks whether you distinguish “never set” from “set to nothing.” Follow-up: “typeof each?” — typeof undefined is "undefined", but typeof null is "object".

Source: MDN — undefined.

What is NaN, and why is Number.isNaN preferred over the global isNaN? Mid

NaN (“not a number”) is the numeric result of an invalid math operation. Its defining oddity: it is not equal to itself, so NaN === NaN is false. The global isNaN() coerces its argument first, so it gives false positives; Number.isNaN() does no coercion and only returns true for an actual NaN.

NaN === NaN          // false
isNaN("hello")        // true  — coerces "hello" to NaN first (misleading)
Number.isNaN("hello") // false — it's a string, not NaN
Number.isNaN(0 / 0)  // true

Why it's asked / follow-up: the self-inequality and the two isNaNs are a classic “do you know the footguns” probe. Follow-up: “how else can you detect NaN?” — Object.is(x, NaN), or the self-inequality trick x !== x.

Source: MDN — Number.isNaN.

2 · Scope, closures & hoisting

What is the difference between var, let, and const? Junior

var is function-scoped, can be redeclared, and is hoisted as undefined. let and const are block-scoped ({ }), can't be redeclared in the same scope, and live in the temporal dead zone until their declaration runs. const forbids reassignment of the binding — it does not freeze the object, so a const array can still be pushed to. Modern code uses const by default and let when reassignment is needed; var is effectively legacy.

const nums = [1, 2];
nums.push(3);     // fine — mutating, not reassigning
nums = [];        // TypeError: Assignment to constant variable

if (true) { var v = 1; let l = 2; }
v;                // 1  — var leaked out of the block
l;                // ReferenceError — let stayed in the block

Why it's asked / follow-up: it's the most basic modern-JS fluency check. Follow-up: “why prefer const?” — it signals intent and prevents accidental reassignment; reach for let only when you truly rebind.

Source: MDN — let. Arrived with ES2015 (ES6).

What is hoisting? Mid

Hoisting is that declarations are processed before any code runs, so a name exists throughout its scope. But what is hoisted differs: var is hoisted and initialized to undefined; function declarations are hoisted whole (callable before their line); let / const are hoisted but stay uninitialized in the temporal dead zone, so touching them early throws.

greet();            // "hi" — function declaration is fully hoisted
function greet() { console.log("hi"); }

console.log(x);     // undefined — var hoisted, not yet assigned
var x = 5;

console.log(y);     // ReferenceError — let is in the TDZ
let y = 5;

Why it's asked / follow-up: it explains both “why was my var undefined” and “why did let throw.” Follow-up: “are function expressions hoisted?” — only the variable is; the function value isn't assigned until that line.

Source: MDN — Hoisting.

What is the temporal dead zone? Mid

The temporal dead zone (TDZ) is the span from the start of a block to the line where a let or const is declared. The name is reserved in scope (so it shadows any outer one), but reading or writing it in that window throws a ReferenceError. It exists to turn “used before declared” into a loud error rather than the silent undefined that var produced.

{
  // TDZ for `total` starts here
  console.log(total);  // ReferenceError, not undefined
  let total = 0;       // TDZ ends — `total` now usable
}

Why it's asked / follow-up: it's the “why is let safer than var” payoff. Follow-up: “does typeof dodge it?” — no; typeof on a TDZ variable still throws, unlike on a never-declared name.

Source: MDN — Temporal dead zone.

What is a closure? Mid

A closure is a function bundled together with the lexical scope it was created in — it keeps access to those outer variables even after the outer function has returned. It's how you get private state, factory functions, and the data-hiding patterns JavaScript had before classes and modules.

function counter() {
  let n = 0;             // private to each counter
  return () => ++n;      // closes over n
}
const next = counter();
next();  // 1
next();  // 2  — n persists between calls, unreachable from outside

Why it's asked / follow-up: closures underpin callbacks, event handlers, and module-style privacy; they're one of the most-asked JS concepts. Follow-up: “what does a closure capture — the value or the variable?” — the variable, which is the cause of the var-in-a-loop trap (see Classic gotchas).

Source: MDN — Closures.

What is an IIFE, and why was it used? Mid

An IIFE (immediately invoked function expression) is a function defined and called on the spot. Before block scoping and modules, it was the standard way to get a private scope — variables inside didn't leak to the global namespace — and to build the “module pattern.” Today let/const blocks and ES modules cover most of its old uses, but you'll still see it in older code and bundler output.

(function () {
  var secret = 42;     // scoped to the IIFE, not global
  // ... setup work ...
})();

const api = (() => {   // module pattern: expose only what you return
  let count = 0;
  return { inc: () => ++count };
})();

Why it's asked / follow-up: it ties together scope, closures, and JS history. Follow-up: “why the wrapping parentheses?” — they make the parser read function as an expression (a value to call) rather than a declaration.

Source: MDN — IIFE.

3 · this, functions & binding

How is the value of this determined? Mid

For an ordinary function, this is set by how the function is called, not where it's defined. Four rules, in order of precedence: called with new → the new object; called with call/apply/bind → the bound value; called as obj.method()obj; called plain → undefined in strict mode (the global object otherwise). Arrow functions ignore all of this and inherit this lexically.

const obj = {
  name: "A",
  hi() { return this.name; }
};
obj.hi();              // "A"  — called as a method
const f = obj.hi;
f();                   // undefined — plain call, `this` is lost

Why it's asked / follow-up: mis-set this is one of the most common JS bugs. Follow-up: “why did f() lose its this?” — detaching the method drops the call-site object (see “losing this” in Classic gotchas).

Source: MDN — this.

What do call, apply, and bind do? Mid

All three set this explicitly. call invokes the function now with arguments listed individually; apply is the same but takes the arguments as an array; bind doesn't invoke — it returns a new function permanently bound to that this (and optionally some leading arguments, i.e. partial application).

function greet(g) { return g + ", " + this.name; }
const user = { name: "Ada" };

greet.call(user, "Hi");    // "Hi, Ada"
greet.apply(user, ["Hi"]);  // "Hi, Ada" — args as an array
const bound = greet.bind(user);
bound("Hey");             // "Hey, Ada" — this is fixed forever

Why it's asked / follow-up: bind is the classic fix for “my callback lost its this.” Follow-up: “call vs apply?” — identical except how arguments are passed (spread ... has made apply largely unnecessary).

Source: MDN — Function.prototype.bind.

How do arrow functions differ from regular functions? Mid

The big one: an arrow function has no own this — it captures this from the enclosing lexical scope, which is exactly what you want for callbacks inside a method. Arrows also have no arguments object, can't be called with new, and have no prototype. Use a regular function when you need a dynamic this (object methods, constructors); use an arrow to preserve the surrounding this.

const timer = {
  secs: 0,
  start() {
    setInterval(() => { this.secs++; }, 1000); // arrow keeps `this` = timer
  }
};
// A regular function here would set this = undefined/global instead.

Why it's asked / follow-up: the lexical-this behavior is the reason arrows exist and a frequent source of “why is this wrong” answers. Follow-up: “when should you NOT use an arrow?” — as an object method or constructor, where you need the call-site this.

Source: MDN — Arrow functions. Arrived with ES2015 (ES6).

What are default, rest, and spread parameters? Junior

Default parameters supply a value when an argument is missing/undefined. Rest (...args in a parameter list) gathers any extra arguments into a real array. Spread (... at a call site or in a literal) expands an iterable into individual elements. Rest and spread share syntax but do opposite things — gather vs. expand.

function sum(first = 0, ...rest) {  // default + rest (array)
  return rest.reduce((a, b) => a + b, first);
}
sum(1, 2, 3);            // 6

const nums = [2, 3];
sum(1, ...nums);         // 6 — spread expands the array into args
const merged = [...nums, 4]; // [2, 3, 4]

Why it's asked / follow-up: they're everywhere in modern code (copying arrays/objects, forwarding args). Follow-up: “rest vs arguments?” — rest is a real Array; arguments is an array-like and absent in arrows.

Source: MDN — Spread syntax / rest parameters.

What does “functions are first-class” mean? Junior

Functions are ordinary values: you can store them in variables, put them in arrays or objects, pass them as arguments, and return them from other functions. That's what makes callbacks, array methods (map/filter), event handlers, and higher-order functions possible. A function that takes or returns a function is a higher-order function.

const ops = { add: (a, b) => a + b };  // stored in an object
[1, 2, 3].map(ops.add.bind(null, 10)); // [11, 12, 13] — passed as a value

const twice = fn => x => fn(fn(x));   // returns a function
twice(n => n + 1)(5);                  // 7

Why it's asked / follow-up: it's the foundation of functional patterns in JS. Follow-up: “what's a higher-order function?” — one that takes a function as an argument or returns one (map, setTimeout, a decorator).

Source: MDN — First-class function.

4 · Prototypes, objects & classes

What is the prototype chain? Mid

Every object has an internal link, [[Prototype]], to another object. When you read a property that isn't on the object itself, the engine walks up this chain until it finds the property or reaches null. That's how method sharing and inheritance work in JavaScript — no classes required underneath. Don't confuse obj.__proto__ (the link an instance follows) with Fn.prototype (the object that becomes the [[Prototype]] of instances created by new Fn()).

const arr = [1, 2];
arr.map;            // found on Array.prototype, not on arr itself
Object.getPrototypeOf(arr) === Array.prototype;  // true
arr.hasOwnProperty("map");  // false — it's inherited, not own

Why it's asked / follow-up: it's the mechanism every JS object model rests on. Follow-up: “__proto__ vs prototype?” — __proto__ is the per-object link; prototype is a property on constructor functions that seeds it.

Source: MDN — Inheritance and the prototype chain.

How does prototypal inheritance work, and how is it different from classical inheritance? Senior

In class-based languages, a class is a blueprint and instances are stamped from it. In JavaScript, objects simply delegate to other objects: an object's [[Prototype]] is itself a live object, and a failed property lookup is forwarded to it. Object.create(proto) makes a new object with a chosen prototype directly — the purest expression of the model. class syntax (next question) is sugar over exactly this.

const animal = { speak() { return this.sound; } };
const dog = Object.create(animal);  // dog delegates to animal
dog.sound = "woof";
dog.speak();        // "woof" — speak found on animal, this = dog

Why it's asked / follow-up: it separates people who memorized class from people who understand what it compiles to. Follow-up: “what does Object.create(null) give you?” — an object with no prototype at all (no inherited toString etc.), handy as a clean map.

Source: MDN — Object.create.

Are JavaScript classes “real” classes? Mid

No — class is syntactic sugar over prototypes. Methods you declare in a class body land on ClassName.prototype; extends wires up the prototype chain; super calls up it. The sugar is worthwhile (clearer syntax, #private fields, a guard against calling a constructor without new), but underneath it's the same delegation model.

class Animal {
  constructor(sound) { this.sound = sound; }
  speak() { return this.sound; }
}
class Dog extends Animal {
  speak() { return super.speak() + "!"; }
}
typeof Dog;                       // "function" — a class is a function
Dog.prototype.speak;             // the method lives on the prototype

Why it's asked / follow-up: it checks whether you know what the sugar desugars to. Follow-up: “what does extends actually do?” — sets the subclass's prototype (and its [[Prototype]] for static members) to the parent.

Source: MDN — Classes. Arrived with ES2015 (ES6).

What does Object.freeze do, and what are its limits? Mid

Object.freeze(obj) makes an object's existing properties non-writable and non-configurable — you can't add, remove, or change them. The key limit: it is shallow. Nested objects are still mutable. (A real deep freeze means recursing yourself.) In strict mode, a write to a frozen property throws; in sloppy mode it fails silently.

const cfg = Object.freeze({ env: "prod", opts: { debug: false } });
cfg.env = "dev";     // ignored (throws in strict mode)
cfg.opts.debug = true; // WORKS — nested object isn't frozen
Object.isFrozen(cfg);   // true

Why it's asked / follow-up: the shallow-ness is the gotcha most people miss. Follow-up: “freeze vs const?” — const stops reassigning the variable; freeze stops mutating the object's own properties. They solve different problems.

Source: MDN — Object.freeze.

5 · Async: callbacks, promises, async/await

What is a Promise, and what are its states? Mid

A Promise is an object representing a value that may not exist yet — the eventual result of an async operation. It is in one of three states: pending, then either fulfilled (with a value) or rejected (with a reason), and once settled it never changes. You react with .then() (success), .catch() (failure), and .finally(); because each returns a new promise, they chain — flattening what used to be nested callbacks.

fetch("/api/user")
  .then(res => res.json())   // returns a promise → chains
  .then(user => render(user))
  .catch(err => show(err))    // any rejection above lands here
  .finally(() => hideSpinner());

Why it's asked / follow-up: promises are the backbone of all modern async JS. Follow-up: “what does returning a value vs a promise inside .then do?” — either way the next .then waits; a returned promise is adopted (unwrapped) before the chain continues.

Source: MDN — Using promises. Arrived with ES2015 (ES6).

How do async / await work? Mid

async/await is syntax over promises that lets you write asynchronous code that reads sequentially. An async function always returns a promise; await pauses the function until the awaited promise settles, then resumes with its value (or throws its rejection). It doesn't block the thread — the function suspends and the event loop keeps running.

async function load() {
  try {
    const res = await fetch("/api/user");
    const user = await res.json();
    return user;             // resolves the returned promise
  } catch (err) {
    // a rejected await is caught here, like sync code
  }
}

Why it's asked / follow-up: it's how most production async code is written now. Follow-up: “how do you run two awaits in parallel?” — start both promises first, then await Promise.all([a, b]) — awaiting them one after another serializes work that could overlap.

Source: MDN — async function. Arrived with ES2017.

How do you handle errors in async code? Mid

With promises, attach .catch() (it handles a rejection anywhere earlier in the chain). With async/await, wrap awaits in try/catch exactly like synchronous code. The common bug is a floating promise — calling an async function without awaiting it or catching it, so a rejection becomes an unhandled rejection.

// BAD: nothing awaits or catches this → unhandled rejection
load();

// GOOD: await inside try/catch, or attach .catch()
try { await load(); }
catch (e) { report(e); }
load().catch(report);   // equivalent for a fire-and-forget call

Why it's asked / follow-up: unhandled rejections are a top source of silent production failures. Follow-up: “does a try/catch catch an error in a non-awaited promise inside the block?” — no; only the awaited one. You must await (or .catch) it.

Source: MDN — Using promises: error handling.

What's the difference between Promise.all, race, allSettled, and any? Senior

All four take an iterable of promises and run them concurrently, differing in how they settle. all — fulfills with all values, or rejects as soon as any one rejects (fail-fast). allSettled — waits for every promise and reports each as fulfilled/rejected (never short-circuits). race — settles as soon as the first one settles, win or lose. any — fulfills with the first fulfilled one, ignoring rejections until all fail.

await Promise.all([a, b]);        // [aVal, bVal] — or throws on first reject
await Promise.allSettled([a, b]);  // [{status,value|reason}, ...]
await Promise.race([req, timeout]);// whichever settles first (timeout pattern)
await Promise.any([m1, m2, m3]);   // first success; AggregateError if all fail

Why it's asked / follow-up: picking the right combinator is a real fluency signal. Follow-up: “which would you use for a fetch with a timeout?” — race against a rejecting timer; “for independent calls where one failing shouldn't kill the rest?” — allSettled.

Source: MDN — Promise.all (and the sibling combinators).

What is “callback hell,” and how do promises fix it? Senior

Callback hell is the deep right-drift you get when async steps each nest inside the previous one's callback — hard to read, and worse, each level needs its own error handling. Promises flatten the nesting into a linear chain with one shared .catch; async/await goes further and makes it read like ordinary sequential code.

// Callback hell:
getUser(id, (u) => {
  getOrders(u, (o) => {
    getItems(o, (i) => { /* ...and error handling at every level */ });
  });
});
// async/await — flat, one try/catch:
const u = await getUser(id);
const o = await getOrders(u);
const i = await getItems(o);

Why it's asked / follow-up: it tells the story of why promises and async/await were added. Follow-up: “is sequential await always right?” — no; if steps are independent, run them concurrently with Promise.all instead of awaiting in series.

Source: MDN — Using promises.

6 · The event loop & concurrency

Explain the event loop. Senior

JavaScript runs on a single thread with a call stack. Slow work (timers, network, DOM events) is handed off to the host (browser / Node), which queues a callback when it's done. The event loop's job: when the stack is empty, take the next queued task and run it to completion. Crucially there are two queues — the macrotask queue (timers, I/O, UI events) and the higher-priority microtask queue (promise reactions, queueMicrotask). After each macrotask the loop drains all microtasks before rendering or taking the next macrotask.

console.log("1");
setTimeout(() => console.log("2"), 0); // macrotask
Promise.resolve().then(() => console.log("3")); // microtask
console.log("4");
// logs 1, 4, 3, 2  — sync first, then microtasks, then the timer

Why it's asked / follow-up: it's the signature senior JavaScript question and the thing the open web most often explains wrong. Follow-up: “why does a long synchronous loop freeze the UI?” — it never yields the stack, so the loop can't process clicks or repaint.

Source: MDN — JavaScript execution model.

Why does Promise.then run before setTimeout(…, 0)? Senior

Because they go in different queues. A setTimeout callback is a macrotask; a promise reaction is a microtask. The event loop empties the entire microtask queue after the current task and before the next macrotask — so any pending .then runs ahead of even a zero-delay timer. (And setTimeout(…, 0) isn't really 0 ms; the spec clamps nested timers to a minimum of ~4 ms.)

setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
queueMicrotask(() => console.log("microtask"));
// promise, microtask, timeout  — both microtasks drain before the timer

Why it's asked / follow-up: it's the precise test of whether you actually understand the two-queue model. Follow-up: “can microtasks starve the loop?” — yes; a microtask that keeps scheduling more microtasks can block rendering and macrotasks indefinitely.

Source: MDN — Using microtasks.

If JavaScript is single-threaded, how can it do things concurrently? Mid

Your JavaScript runs on one thread, but the host environment isn't single-threaded. When you call fetch or setTimeout, the work is handed to the browser/Node, which runs it elsewhere and queues a callback when done. So JS is “concurrent” in the sense of not blocking on I/O, not in the sense of running your code in parallel. For genuine CPU parallelism you reach for Web Workers (or worker threads in Node), which run on separate threads and communicate by messages.

// The fetch happens off-thread; this line doesn't block:
fetch("/slow").then(handle);
doOtherWork();      // runs immediately, before the response arrives

// True parallelism: offload CPU work to another thread
const worker = new Worker("crunch.js");

Why it's asked / follow-up: it separates “non-blocking” from “parallel,” a distinction people routinely blur. Follow-up: “how do workers share data?” — by message passing (structured clone), or a SharedArrayBuffer for shared memory.

Source: MDN — JavaScript execution model.

When an async function hits await, what actually happens? Senior

await doesn't block. The function runs synchronously up to the await, then suspends and returns control to the caller; everything after the await is scheduled as a microtask that resumes once the awaited promise settles. So code after an await always runs in a later microtask tick, even if the awaited value was already available.

async function f() {
  console.log("A");            // runs synchronously
  await null;                    // suspend; rest becomes a microtask
  console.log("C");            // runs after the current stack unwinds
}
f();
console.log("B");
// logs A, B, C — "C" is deferred to a microtask

Why it's asked / follow-up: it connects async/await to the microtask model from the previous questions. Follow-up: “does await on a non-promise still defer?” — yes; the value is wrapped and the continuation is queued as a microtask regardless.

Source: MDN — await.

7 · Arrays, iteration & functional methods

What do map, filter, and reduce do? Junior

All three are non-mutating array methods that take a callback. map transforms each element and returns a new array of the same length. filter keeps the elements for which the callback returns truthy. reduce folds the array down to a single accumulated value. They return new arrays/values, leaving the original untouched.

const nums = [1, 2, 3, 4];
nums.map(n => n * 2);              // [2, 4, 6, 8]
nums.filter(n => n % 2 === 0);     // [2, 4]
nums.reduce((sum, n) => sum + n, 0); // 10

Why it's asked / follow-up: they're the everyday functional toolkit and a smell test for idiomatic JS. Follow-up: “why pass an initial value to reduce?” — it avoids a throw on an empty array and fixes the accumulator's type.

Source: MDN — Array.prototype.reduce (and map / filter).

How do you copy or merge an array without mutating it? Junior

Spread (...) is the idiomatic shallow copy and merge: [...arr] clones, [...a, ...b] concatenates. Modern non-mutating methods help too: toSorted, toReversed, and with return new arrays instead of changing the original (unlike the old sort/reverse, which mutate in place). Remember spread is shallow — nested objects are shared.

const a = [3, 1, 2];
const copy = [...a];            // shallow clone
const sorted = a.toSorted();    // [1, 2, 3] — a is untouched
a.sort();                       // MUTATES a in place — older API

Why it's asked / follow-up: accidental mutation (especially of state in UI frameworks) is a frequent bug. Follow-up: “does spread deep-copy?” — no; use structuredClone() for a deep copy.

Source: MDN — Spread syntax.

What is destructuring? Mid

Destructuring unpacks values from arrays or properties from objects into distinct variables in one expression, with support for defaults, renaming, and nesting. It's heavily used for pulling fields out of objects, swapping variables, and giving function parameters a self-documenting shape.

const { name, role = "dev" } = user;   // object, with a default
const [first, , third] = items;          // array, skipping one
function draw({ x = 0, y = 0 } = {}) {}   // destructured params
[a, b] = [b, a];                         // swap without a temp

Why it's asked / follow-up: it's pervasive in modern code, especially around props and config objects. Follow-up: “why the = {} on the parameter?” — so calling with no argument doesn't throw when it tries to destructure undefined.

Source: MDN — Destructuring assignment. Arrived with ES2015 (ES6).

What are iterators and generators? Senior

An object is iterable if it implements Symbol.iterator, returning an iterator — an object with a next() that yields { value, done }. That protocol is what for...of, spread, and destructuring all consume. A generator (function*) is the easy way to build one: each yield produces a value and pauses, so values are computed lazily — even infinitely.

function* ids() {
  let n = 1;
  while (true) yield n++;   // lazy, infinite — only computes on demand
}
const g = ids();
g.next().value;   // 1
g.next().value;   // 2

Why it's asked / follow-up: it explains how for...of works and unlocks lazy/streaming patterns. Follow-up: “how do you make your own object iterable?” — give it a [Symbol.iterator]() method (often itself a generator).

Source: MDN — Iteration protocols.

What's the difference between for...of and for...in? Mid

for...of iterates the values of an iterable (arrays, strings, Maps, Sets). for...in iterates the enumerable property keys of an object — including inherited ones — and on an array gives you index strings, not elements. Rule of thumb: for...of for arrays/iterables, for...in only for plain-object keys (and even then, prefer Object.keys).

const arr = ["a", "b"];
for (const v of arr) {}   // "a", "b"  — values
for (const k in arr) {}   // "0", "1" — index strings (+ any added keys)
for (const k of Object.keys(obj)) {} // safe way to walk an object's own keys

Why it's asked / follow-up: using for...in on an array is a classic bug. Follow-up: “why can for...in surprise you?” — it walks the prototype chain's enumerable keys too, so a polyfill on Array.prototype would show up.

Source: MDN — for...in / for...of.

8 · DOM & browser events

Why use addEventListener instead of onclick? Mid

An onclick property holds a single handler — assigning a new one overwrites the old. addEventListener lets you attach multiple handlers, choose the capture vs. bubble phase, pass options like { once: true } or { passive: true }, and removeEventListener later. It also keeps behavior out of HTML attributes.

function onClick() { /* ... */ }
btn.addEventListener("click", onClick);
btn.addEventListener("click", logIt);     // both run — not overwritten
btn.removeEventListener("click", onClick); // needs the same reference

Why it's asked / follow-up: it's the baseline of DOM event handling. Follow-up: “why can't you remove an inline arrow handler?” — removeEventListener needs the exact same function reference, which an inline arrow doesn't give you.

Source: MDN — EventTarget.addEventListener.

Explain event bubbling vs. capturing, and stopPropagation vs. preventDefault. Mid

A dispatched event travels in three phases: capture (document down to the target), target, then bubble (target back up). Handlers run on bubble by default; pass { capture: true } to run on the way down. event.stopPropagation() halts that travel (other elements' handlers won't fire); event.preventDefault() cancels the browser's default action (following a link, submitting a form) but lets propagation continue. They're independent.

form.addEventListener("submit", e => {
  e.preventDefault();    // stop the page reload, keep handling the event
});
child.addEventListener("click", e => {
  e.stopPropagation();   // parent's click handler won't see this
});

Why it's asked / follow-up: conflating the two methods is extremely common. Follow-up: “what does stopImmediatePropagation add?” — it also stops other handlers on the same element from running, not just ancestors.

Source: MDN — Event.stopPropagation / preventDefault.

What is event delegation, and why does it scale? Senior

Event delegation attaches a single listener to a common ancestor and uses event.target to figure out which descendant was acted on — relying on bubbling. It scales because you avoid N listeners on N children, and it automatically covers elements added after the listener was attached (which per-element handlers wouldn't).

list.addEventListener("click", (e) => {
  const item = e.target.closest("li");
  if (!item) return;        // click wasn't on/inside an <li>
  select(item.dataset.id);   // works for items added later, too
});

Why it's asked / follow-up: it's the standard answer to “how do you handle clicks on a dynamic list efficiently.” Follow-up: “why closest() rather than checking e.target directly?” — the click may land on a child element inside the row, so you walk up to the row you care about.

Source: MDN — Event delegation.

What is the difference between debounce and throttle? Mid

Both limit how often a handler runs on a high-frequency event (scroll, resize, input). Debounce waits until the event has stopped for N ms, then fires once — ideal for search-as-you-type. Throttle fires at most once per N ms during a burst — ideal for scroll position updates.

function debounce(fn, ms) {
  let t;
  return (...args) => {
    clearTimeout(t);                       // reset the timer on each call
    t = setTimeout(() => fn(...args), ms); // fire only after a quiet gap
  };
}
search.addEventListener("input", debounce(query, 300));

Why it's asked / follow-up: it's a practical performance question with a tiny closure-based implementation interviewers like to see written. Follow-up: “which for a typeahead vs. a scroll spy?” — debounce the typeahead, throttle the scroll spy.

Source: MDN — Scroll event throttling.

9 · Modules & tooling

What's the difference between ES modules and CommonJS? Mid

ES modules (ESM) — the standard import/export syntax — are static (bindings resolved before execution, enabling tree-shaking), the imports are live read-only bindings, and they load asynchronously. CommonJS — Node's older require/module.exports — is dynamic and synchronous, and require returns a copied value. ESM is the standard for browsers and modern Node; CommonJS remains widespread in the Node ecosystem.

// ES modules (standard)
import { sum } from "./math.js";
export function sum(a, b) { return a + b; }

// CommonJS (Node legacy)
const { sum } = require("./math");
module.exports = { sum };

Why it's asked / follow-up: the ESM/CJS split is a daily friction in Node projects. Follow-up: “why can't you require an ESM file directly?” — ESM is async and statically analyzed; the interop is via dynamic import() or .mjs/"type": "module".

Source: MDN — JavaScript modules. Standardized in ES2015 (ES6).

What's the difference between a default and a named export? Mid

A module can have any number of named exports (imported by their exact name in braces) and at most one default export (imported under any name you choose, no braces). Named exports help tooling and refactors because the names are fixed; defaults are convenient for a module's single main thing.

// exporter
export const PI = 3.14159;       // named
export default function area(r) {} // default

// importer
import area, { PI } from "./circle.js"; // default + named, renamable

Why it's asked / follow-up: import/export mistakes are a routine source of build errors. Follow-up: “default or named — which do you prefer?” — many teams favor named exports for grep-ability and consistent naming; there's no single right answer, just trade-offs.

Source: MDN — export.

What is dynamic import(), and when would you use it? Senior

Static import runs at the top of a module and loads eagerly. Dynamic import() is a function-like form that returns a promise for the module, so you can load code on demand — behind a route, a click, or a feature flag. That's the basis of code splitting: ship a small initial bundle and pull in the rest only when needed.

button.addEventListener("click", async () => {
  const { renderChart } = await import("./chart.js"); // loaded on click
  renderChart(data);
});

Why it's asked / follow-up: lazy-loading is central to modern web performance. Follow-up: “where can static import not go?” — it can't be conditional or inside a function; that's exactly the gap dynamic import() fills.

Source: MDN — import() (dynamic import). Standardized in ES2020.

Why do bundlers and transpilers exist? Senior

A transpiler (Babel, the TypeScript compiler, swc) rewrites newer or non-standard syntax into a form older engines understand — modern JS down-leveled, JSX into function calls, TypeScript stripped to JS. A bundler (Vite/Rollup, esbuild, webpack) follows the import graph and combines many modules into a few optimized files, doing tree-shaking, minification, and code-splitting along the way. Native ESM reduced the need for bundling in dev, but production builds still benefit from the optimization passes.

// What a transpiler down-levels (modern → broadly supported):
const f = (x) => x?.value ?? 0;     // optional chaining + nullish coalescing
// → var f = function (x) { ... }   (ES5-compatible output)

Why it's asked / follow-up: it probes whether you understand the build pipeline you rely on daily. Follow-up: “is TypeScript a transpiler?” — its compiler is; it type-checks and emits JS (see the TypeScript version reference).

Source: MDN — JavaScript modules.

10 · Classic gotchas

Why is 0.1 + 0.2 !== 0.3? Mid

JavaScript numbers are IEEE-754 double-precision floats (it has just one number type). Values like 0.1 and 0.2 can't be represented exactly in binary, so their sum is a hair off 0.3. This is not a JS bug — it's how binary floating point works everywhere. Compare with a tolerance (Number.EPSILON), or use integers (cents) or a decimal library for money.

0.1 + 0.2;                     // 0.30000000000000004
0.1 + 0.2 === 0.3;             // false
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON;  // true — compare with tolerance

Why it's asked / follow-up: it checks whether you understand the single float number type and never compute money in floats. Follow-up: “how do you handle currency?” — integer minor units (cents) or a decimal/BigInt-based approach.

Source: MDN — Number.EPSILON.

Why is typeof null === "object"? Mid

It's a bug from the very first JavaScript implementation: values were tagged by a few low bits, the object tag was 0, and null was the null pointer (all zero bits) — so it read as an object. A fix was proposed and rejected because it would break too much existing code, so it's enshrined forever. Check for null explicitly, not via typeof.

typeof null;                    // "object"  ← the historical bug
// correct null check:
value === null;
// "is it a real object?" guard:
value !== null && typeof value === "object";

Why it's asked / follow-up: it's a JS-trivia favorite that also has a practical guard attached. Follow-up: “how do you test for a plain object then?” — rule out null first, then typeof x === "object" (and often !Array.isArray(x)).

Source: MDN — typeof null.

Why do all the callbacks in this var loop log the same number? Senior

With var there is one shared i for the whole function, and every closure captures that same variable. By the time the timeouts run, the loop has finished and i holds its final value. The modern fix is let, which creates a fresh binding per iteration.

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3, 3, 3 — one shared i
}
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 0, 1, 2 — fresh i each iteration
}

Why it's asked / follow-up: it's the canonical closures-plus-scope question and the best argument for let. Follow-up: “how was this solved before let?” — an IIFE per iteration capturing i by value, or forEach (whose callback gets its own parameter).

Source: MDN — Closures in loops.

Why does passing a method as a callback “lose” this? Mid

Because this is set by the call site, not the definition. obj.method is just a function reference; once it's detached — handed to setTimeout, an event listener, or map — it's called plain, so this is undefined (strict) or the global object. Fix it by binding or wrapping in an arrow that preserves the outer this.

const counter = { n: 0, inc() { this.n++; } };
setTimeout(counter.inc, 0);            // `this` is lost → NaN / error
setTimeout(counter.inc.bind(counter), 0); // bound — works
setTimeout(() => counter.inc(), 0);       // arrow keeps the call — works

Why it's asked / follow-up: it's the practical payoff of the “how is this determined” question. Follow-up: “how do class components avoid it?” — class fields with arrow functions, or binding in the constructor.

Source: MDN — this.

Why is [] == ![] true? Senior

It's coercion stacked on coercion. ![] is false (a non-empty object is truthy, so its negation is false). Now [] == false: == converts the boolean to 0, then converts the array to a primitive — [] becomes "", which becomes 0. So it's 0 == 0true. The lesson isn't to memorize the rules; it's to use === so none of this happens.

![];          // false  (arrays are truthy)
[] == ![];    // true   ([]→""→0, false→0)
[] === ![];   // false  — strict equality: no coercion
"1" + 1;      // "11"   (+ prefers string concat)
"1" - 1;      // 0      (- forces numeric)

Why it's asked / follow-up: it's a stress-test of the coercion rules — and the “just use ===” takeaway is the point. Follow-up: “why does + concatenate but - subtract on strings?” — + is overloaded for strings and prefers concatenation; the other arithmetic operators always coerce to number.

Source: MDN — Equality comparisons and sameness.

Every answer links its primary source inline — MDN Web Docs and the ECMA-262 specification (TC39). The questions are a curated set of the topics a JavaScript interviewer commonly covers, not a copy of any question bank. Language-feature details cross-link to the JavaScript version reference; typing and tooling notes cross-link to the TypeScript version reference. Last updated June 2026.

Mungomash LLC · More on JavaScript

Last refreshed 2026-06-23 by Mimas — new page: JavaScript interview questions.