← All languages
JavaScript interview questions
40 curated JavaScript concept questions with model answers. Practice them out loud in the scored Fundamentals room.
JavaScript interviews reward candidates who know how the runtime actually behaves, not just how to call the API. Expect closures, the event loop, prototypes, and the difference between microtasks and macrotasks to come up in almost every loop, because those are the places where surprising bugs live.
- Closures and the module pattern: why a function keeps access to variables after its outer scope returns, and how that powers callbacks and data privacy.
- The event loop, microtasks, and macrotasks: predict the output order of setTimeout, Promise.then, and async/await in the same snippet.
- this, prototypes, and class syntax: explain how this is bound in regular vs arrow functions and how inheritance really works under the hood.
What interviewers are really testing
Interviewers use JavaScript questions to find out whether you understand the runtime or have only memorised syntax. The signal they look for is precise reasoning about when code runs — synchronous, microtask, or macrotask — and why a closure still sees its binding long after the loop that created it has finished.
Basic
What is the difference between var, let, and const?
- var is function-scoped and hoisted with an initial value of undefined; let/const are block-scoped and sit in the temporal dead zone until their declaration executes.
- let allows reassignment; const forbids reassignment of the binding (but an object it points to can still be mutated).
- Redeclaring a let/const in the same scope is a SyntaxError; redeclaring var silently overwrites.
- let/const also create a fresh binding per loop iteration, which is why closures in for...let loops capture the right value.
- Prefer const by default, let when the reference truly changes, and avoid var in modern code.
When would you use == versus ===?
- === is strict equality: it compares type and value with no coercion. == is loose equality and applies the abstract equality algorithm first.
- == coerces operands (e.g. '' == 0, '1' == 1, null == undefined are all true), which makes results hard to predict.
- Always use === except for the one deliberate idiom `x == null`, which is true for both null and undefined.
- Object.is is a third variant that differs from === only for NaN (Object.is(NaN, NaN) is true) and +0/-0.
- NaN is not equal to anything, including itself, so use Number.isNaN to test for it.
What's the difference between null and undefined?
- undefined means 'no value assigned yet' — it is the default for uninitialized variables, missing object properties, and functions with no return value.
- null is an explicit, intentional 'no value' that the author assigns.
- typeof undefined is 'undefined', but typeof null is 'object' (a long-standing language bug).
- They are loosely equal (null == undefined) but not strictly equal.
- Optional chaining (?.) and nullish coalescing (??) are the modern way to guard against both; ?? only falls back for null/undefined, unlike || which also falls back for 0, '' and false.
What does typeof return, and where is it misleading?
- typeof returns one of 'undefined', 'object', 'boolean', 'number', 'bigint', 'string', 'symbol', or 'function'.
- typeof null === 'object' — a historical bug that was never fixed for backwards compatibility.
- Arrays and dates also report 'object', so use Array.isArray() for arrays.
- typeof a function is 'function' even though functions are objects.
- For a general type check prefer Array.isArray, instanceof, or Object.prototype.toString.call(x).
Explain map, filter, and reduce with an example of each.
- map transforms every element and returns a new array of the same length; it does not mutate the original.
- filter keeps only elements whose callback returns truthy, returning a shorter array.
- reduce folds the array into a single value using an accumulator and an initial value — summing, grouping, or building an object.
- All three are non-mutating and chainable, which makes data pipelines readable.
- Always pass an initial value to reduce; without one it throws on an empty array and uses the first element as the seed otherwise.
How does `this` differ between a regular function and an arrow function?
- A regular function's `this` is decided at call time: the object before the dot, the new instance with `new`, or the global/undefined when called bare.
- Arrow functions have no own `this`; they capture it lexically from the enclosing scope at definition time.
- That makes arrows ideal for callbacks (map, setTimeout) where you want the outer `this`.
- Arrows also cannot be used as constructors and have no `arguments` object or `prototype`.
- call/apply/bind can override `this` for regular functions but are ignored by arrow functions.
What are the falsy values in JavaScript?
- The falsy values are false, 0, -0, 0n (BigInt zero), '' (empty string), null, undefined, and NaN.
- Everything else is truthy — including '0' (non-empty string), 'false', [] and {} (all objects are truthy).
- This is why `if ([])` runs but `[] == false` is also true: the second uses coercion, not truthiness.
- Guard optional values with `??` rather than `||` when 0, '' or false are valid.
- Avoid relying on implicit coercion for readability; be explicit with Boolean(x) or a comparison.
What's the difference between the spread and rest operators, which both use `...`?
- Rest collects remaining items into an array — in function parameters (`...args`) and destructuring (`const [a, ...rest] = arr`).
- Spread expands an iterable in place — function calls (`fn(...args)`), array literals (`[...a, ...b]`), and object literals (`{...obj}`).
- Both use `...`; the position and context decide which one it is.
- Object spread and Object.assign do a shallow copy; nested objects/arrays are still shared references.
- Rest parameters must be last in a parameter list.
How do you copy an object, and what are the pitfalls?
- The spread `{...obj}` and Object.assign({}, obj) create a shallow copy: top-level keys are copied, nested objects are shared by reference.
- Mutating a nested object in the copy also mutates the original.
- structuredClone(obj) does a deep clone and handles Dates, Maps, Sets and cycles; it cannot clone functions.
- The old JSON.parse(JSON.stringify(obj)) trick is a deep clone but drops undefined, functions, Dates become strings, and it fails on cycles.
- For immutability prefer updating by creating new objects along the path you change.
When would you use a Map instead of a plain object?
- Map keys can be any type (objects, functions, NaN), while object keys are strings or symbols.
- Map preserves insertion order and exposes .size, .get, .set, .has, .delete.
- Map is optimized for frequent additions and deletions; a plain object is fine for static, string-keyed records.
- Plain objects inherit from Object.prototype, so keys like 'toString' can collide; Map has no prototype keys.
- Use objects for JSON-shaped data and Map for a dynamic lookup table.
Why does 1 + '2' equal '12' while 1 - '2' equals -1?
- + is overloaded: if either operand is a string it concatenates, so a number is coerced to a string first.
- - is numeric-only, so it coerces both operands to numbers (NaN if they can't convert).
- The abstract operations are ToPrimitive then ToString for + and ToNumber for -, * and /.
- [] + {} and {} + [] surprise people for the same reason — + is not just addition.
- Using == with mixed types runs similar coercion and is why === is safer.
What can template literals do that string concatenation can't?
- They interpolate expressions with ${...} inline, keeping the string readable.
- They support multi-line strings without escape characters.
- Tagged templates let a function receive the parts and values (used by styled-components and sql helpers).
- They evaluate the expression at runtime, like concatenation.
- They can nest and can call functions in the placeholder.
Explain array and object destructuring, including defaults and renaming.
- const { a, b } = obj and const [x, y] = arr extract values by position or key.
- Defaults apply when the value is undefined: const { a = 1 } = obj.
- Rename with a colon: const { a: first } = obj.
- Nested destructuring works and can mix with rest: const { a, ...rest } = obj.
- Destructuring also works in function parameters and can swap variables: [a, b] = [b, a].
How do default parameters work and when is the default evaluated?
- A parameter default runs only when the argument is undefined (not null, 0, or '').
- Defaults are evaluated left to right at call time, and later defaults can use earlier parameters.
- Defaults can be any expression, including a function call, so they are not compile-time constants.
- Passing undefined explicitly triggers the default; passing null does not.
- This replaces the old `x = x || fallback` pattern which wrongly caught 0 and ''.
What do optional chaining (?.) and nullish coalescing (??) do?
- a?.b returns undefined instead of throwing when a is null or undefined; it short-circuits the whole chain.
- It also works for calls a?.() and computed access a?.[i].
- ?? returns the right side only when the left is null or undefined.
- ?? differs from ||, which also falls back for 0, '', false and NaN.
- They can't be mixed with &&/|| without parentheses — that's a syntax error by design.
What do Object.keys, Object.values, and Object.entries return?
- They return arrays of own enumerable string-keyed properties: keys, values, or [key, value] pairs.
- Object.entries is the standard way to loop an object with for...of.
- They ignore the prototype chain (unlike for...in, which includes inherited enumerable keys).
- Symbol keys are excluded; use Object.getOwnPropertySymbols for those.
- Use Object.fromEntries to rebuild an object from a transformed entries array.
When would you use for, for...of, for...in, or forEach?
- Classic for gives full control (index, break, continue) and is the fastest for loops.
- for...of iterates values of any iterable (arrays, strings, maps, sets) and supports break/await.
- for...in iterates enumerable keys, including inherited ones, and is intended for objects, not arrays.
- forEach is concise but cannot break or await; it always returns undefined.
- Use map/filter/reduce when you want a new value rather than a side effect.
How do JSON.parse and JSON.stringify work, and what do they lose?
- JSON.parse turns a JSON string into a value and throws SyntaxError on malformed input.
- JSON.stringify turns a value into a JSON string; it throws on cyclic structures.
- stringify drops undefined, functions and symbols in objects and converts them to null inside arrays.
- Dates become ISO strings, NaN/Infinity become null, and Map/Set become {}.
- Always wrap JSON.parse in try/catch for untrusted input.
What's the difference between slice, substring, and the deprecated substr?
- slice(start, end) accepts negative indices (counted from the end) and clamps out-of-range values.
- substring(start, end) treats negative as 0 and swaps the arguments if start > end.
- substr(start, length) takes a length, not an end index, and is deprecated.
- All return a new string because strings are immutable.
- Prefer slice for its predictable negative-index behavior.
How do you throw and catch a custom error?
- Extend Error, set this.name, and call super(message) so the stack is captured.
- throw new MyError('...') and catch (error) { if (error instanceof MyError) ... }.
- Catch the narrowest type you can; a bare catch also swallows programming bugs.
- Errors are values you can attach data to (e.g. a status code).
- Always re-throw with `throw error` (or `throw` inside catch) to preserve the stack when you only meant to log.
Advanced
Explain the event loop, macrotasks, and microtasks.
- JavaScript runs on a single thread; async work is handed to the host (browser/Node) and callbacks are queued.
- The event loop drains the microtask queue completely after each macrotask and after each callback.
- Microtasks are promise callbacks, queueMicrotask, and MutationObserver; macrotasks are setTimeout, setInterval, I/O, and events.
- That ordering means a resolved promise (microtask) runs before a `setTimeout(…, 0)` (macrotask).
- Long synchronous work blocks the loop and delays every pending callback — hence chunking or workers.
What is a closure and why is it useful?
- A closure is a function that retains access to its lexical scope even after the outer function has returned.
- It lets you create private state — the classic counter returned from a factory function.
- Every function in JavaScript closes over its scope; closures appear naturally with callbacks and partial application.
- Closures keep referenced variables alive, which is also how unintended memory retention happens.
- They power currying, memoization, event handlers, and module patterns.
Explain prototypal inheritance and the prototype chain.
- Every object has an internal [[Prototype]] link to another object (or null); property lookup walks that chain until it finds the key or reaches null.
- Functions have a .prototype object; instances created with `new` get their [[Prototype]] set to that object.
- class syntax is sugar over this: methods live on the prototype, not on each instance.
- Child.prototype = Object.create(Parent.prototype) wires inheritance; Object.getPrototypeOf / Object.setPrototypeOf inspect or change the link.
- Shadowing means an own property hides a prototype property of the same name — the usual source of 'why is my method wrong?' bugs.
Compare callbacks, promises, and async/await.
- Callbacks are the raw mechanism but produce nesting ('callback hell') and error handling is manual at every level.
- A promise is a placeholder for a future value with three states (pending, fulfilled, rejected) and a single .then/.catch/.finally chain.
- async/await is syntax over promises that lets you write sequential-looking code; an async function always returns a promise.
- Errors: await a rejected promise throws, so wrap in try/catch; .catch handles the chain.
- Parallelism: Promise.all rejects fast, Promise.allSettled waits for all, Promise.race settles with the first — choose deliberately, and await together rather than serially when independent.
What's the difference between debounce and throttle?
- Debounce delays the call until the events stop for a given wait — good for search-as-you-type and resize-settled work.
- Throttle guarantees at most one call per interval while events keep firing — good for scroll and mousemove handlers.
- Both reduce how often an expensive function runs; they differ in the shape of the resulting firing pattern.
- Both need cleanup (clearTimeout / cancel) on unmount to avoid leaks and stale callbacks.
- A leading edge option fires immediately then suppresses; trailing edge fires after the quiet period.
How do call, apply, and bind differ?
- All three set `this` for a function invocation.
- call(thisArg, a, b) invokes immediately with individual arguments.
- apply(thisArg, [a, b]) invokes immediately with an array (or array-like) of arguments.
- bind(thisArg, a) returns a new function with `this` (and optionally some arguments) permanently bound; it does not invoke.
- bind is useful for passing methods as callbacks so their `this` is preserved; the bound function cannot be re-bound.
What is a generator function and when would you use one?
- Declared with function*, it returns an iterator and pauses at each yield, resuming where it left off on .next().
- It lets you produce a lazy, potentially infinite sequence without materializing it in memory.
- yield is two-way: value flows out, and a value passed to .next(x) flows back in as the result of yield.
- Generators implement the iterable/iterator protocol, so they work directly in for...of and spread.
- Good fits: streaming/paging data, custom iterators, and cooperative task scheduling.
Does Object.freeze make an object fully immutable?
- Object.freeze prevents adding, removing, or reassigning top-level properties, but it is shallow.
- A frozen object's nested objects and arrays remain mutable.
- It also only stops writes in non-strict mode silently; in strict mode (modules) writes throw.
- For deep immutability you must recursively freeze or use structuredClone into a new object for updates.
- Freeze is a runtime guard, not a type-level guarantee; a TypeScript `readonly` type is erased at runtime.
How can closures or listeners cause memory leaks, and how do you prevent them?
- A closure keeps every variable it references alive; a long-lived timer or listener can retain large data indefinitely.
- DOM listeners registered on elements that outlive the component (window, document) leak if never removed.
- Detached DOM nodes referenced by JS stay in memory; break references when removing them.
- WeakMap/WeakSet hold keys weakly so entries can be collected when the key is otherwise unreachable.
- Prevention: clean up in effect teardown (removeEventListener, clearInterval, AbortController), and avoid capturing more state than needed.
Walk through the rules that decide `this` in a regular function call.
- new binding: a function called with `new` gets a brand-new object as `this`.
- Explicit binding: call/apply/bind set `this` explicitly.
- Implicit binding: `obj.method()` sets `this` to obj; a bare call loses that binding.
- Default binding: a plain function call in non-strict mode gets the global object, in strict mode/modules undefined.
- Precedence is new > explicit > implicit > default, and arrow functions skip all of it.
How can microtasks starve rendering?
- The microtask queue is drained completely before the browser can paint or run timers.
- A chain of resolved promises that keeps queueing microtasks can block rendering indefinitely.
- Recursive Promise.resolve().then(...) loops are the classic example.
- Fix it by yielding to a macrotask (setTimeout) or requestAnimationFrame periodically.
- This is why CPU-heavy loops should be chunked or moved to a Worker.
What are Proxy and Reflect used for?
- Proxy wraps an object and intercepts operations (get, set, has, apply) via traps.
- Reflect provides the default behavior for those operations, so traps can delegate with Reflect.get(target, key, receiver).
- Common uses: validation, observability, virtual properties, and negative array indices.
- Proxy is transparent to code that reads the object; Reflect pairs with it to keep invariants.
- Overuse hurts performance and debuggability.
When would you use WeakMap or WeakSet instead of Map or Set?
- WeakMap keys and WeakSet values must be objects and are held weakly, so they don't prevent garbage collection.
- They are not enumerable — no size, keys, or iteration — by design.
- Good for attaching metadata to DOM nodes or objects without leaking them.
- A Map keyed by the same object keeps it alive forever, which is the leak WeakMap avoids.
- Only the key is weak in WeakMap; the value can still reference the key (careful of cycles).
What is currying and how does it differ from partial application?
- Currying turns f(a, b, c) into f(a)(b)(c) — one argument per call.
- Partial application fixes some arguments now and passes the rest later.
- Both rely on closures to remember the captured arguments.
- They enable reusable, composable functions and pipelines.
- Libraries like lodash.curry implement it; you rarely need true currying over a partial application.
How would you memoize an expensive function?
- Cache results keyed by the arguments, usually in a Map inside a closure.
- Invalidate or bound the cache or it becomes a memory leak.
- Only pure functions with stable, hashable arguments are safe to memoize.
- React's useMemo/useCallback are memoization primitives scoped to a render.
- Serialize object arguments carefully — a new object each call defeats the cache.
Explain event delegation, bubbling, and capturing.
- Events propagate in three phases: capture (down), target, then bubble (up).
- Bubbling lets a single listener on a parent handle events from many children via event.target.
- Delegation gives better performance and works for elements added later.
- stopPropagation stops further propagation; preventDefault cancels the default action (different things).
- addEventListener's third argument (or { capture: true }) opts into the capture phase.
Compare structuredClone, JSON cloning, and the spread operator for copies.
- Spread and Object.assign are shallow — nested objects are shared.
- structuredClone deep-clones and handles Date, Map, Set, ArrayBuffer and cycles.
- JSON parse(stringify()) deep-clones plain data but destroys Date, Map, Set and drops undefined.
- structuredClone cannot clone functions, DOM nodes, or class prototypes (prototype is lost).
- Choose based on whether the data is plain JSON or richer structures.
How do ES modules differ from CommonJS?
- ESM uses import/export and is statically analyzable, enabling tree-shaking and hoisting.
- CommonJS uses require/module.exports and loads synchronously at runtime.
- ESM exports live bindings (the value updates); CommonJS exports a snapshot of the value.
- ESM top-level await and import() dynamic imports are async-friendly.
- In ESM, `this` at top level is undefined and strict mode is always on.
Explain the iterator and iterable protocols.
- An iterable has a [Symbol.iterator] method returning an iterator.
- An iterator has next() returning { value, done }.
- for...of, spread, and destructuring consume any iterable.
- Generators and built-ins (arrays, strings, Maps, Sets) implement these protocols.
- You can make custom objects iterable by defining [Symbol.iterator].
Explain hoisting for var, let, const, function declarations, and classes.
- var declarations are hoisted and initialized to undefined; the assignment stays where it is.
- let/const are hoisted but uninitialized in the temporal dead zone until their declaration runs.
- Function declarations are fully hoisted and callable before their line.
- Function expressions and arrow functions assigned to const are not callable before the assignment.
- Class declarations are hoisted but uninitialized like let, so using them early throws.
Common mistakes
- Saying a Promise callback runs "after" setTimeout without naming the microtask queue.
- Describing closures as "the function remembers variables" instead of the lexical environment it captured.
- Confusing this in arrow functions and regular functions, or claiming arrow functions bind their own this.
- Treating == and === as interchangeable, or forgetting that objects compare by reference.
How to rehearse these answers
- Rewrite one snippet three ways — callback, Promise, async/await — and predict the exact console order before running it.
- Write a small counter factory that returns increment and get functions, then explain which variable each closure keeps alive.
- Implement a tiny debounce and justify why it must capture both a timer id and the latest arguments.
- Explain prototypal inheritance by hand for a two-level chain, then confirm it with Object.getPrototypeOf.
New to technical interviews? Read how to rehearse coding and system design.
Other languages
Python interview questions · Java interview questions · C++ interview questions · C# interview questions · Go interview questions