JavaScript Interview Questions and Answers (2026)
JavaScript interviews reward understanding *why* the language behaves the way it does — closures, the event loop, and coercion rules trip up even experienced developers. Here are the JavaScript questions that come up most often, with answers that go past the one-line definition.
-
What is a closure, and can you give a practical example?
A closure is a function that retains access to variables from its enclosing lexical scope even after that outer function has returned. JavaScript creates closures every time a function is defined, because functions carry a reference to their surrounding scope, not a snapshot of values. This is the mechanism behind data privacy and factory functions.
function makeCounter() { let count = 0; return () => ++count; } const counter = makeCounter(); counter(); // 1 counter(); // 2countisn't accessible from outside, but the returned function still reads and mutates it. Closures are also the classic source of the "loop variable" bug whenvaris used instead ofletinsidesetTimeoutcallbacks. -
Explain the event loop, and the difference between microtasks and macrotasks.
JavaScript is single-threaded and uses an event loop to handle async work. The call stack executes synchronous code first. Once it's empty, the loop drains the microtask queue completely (Promise
.then/.catch/.finally,queueMicrotask,async/awaitcontinuations) before touching the macrotask queue (setTimeout,setInterval, I/O, UI rendering,setImmediatein Node). This repeats every tick.console.log('1'); setTimeout(() => console.log('2'), 0); Promise.resolve().then(() => console.log('3')); console.log('4'); // Output: 1, 4, 3, 2Microtasks are prioritized because they represent "finish what's already in flight" work, while macrotasks represent scheduling new work. Starving the macrotask queue with recursive microtasks can block rendering entirely.
-
How does `this` get determined in JavaScript?
thisis determined by how a function is called, not where it's defined (except for arrow functions). The rules, in priority order: (1)new Foo()—thisis the newly created object; (2)fn.call(obj)/.apply(obj)/.bind(obj)—thisis explicitly set; (3)obj.method()—thisisobj(implicit binding); (4) plain function call —thisisundefinedin strict mode, or the global object otherwise. Arrow functions don't have their ownthis; they lexically inherit it from the enclosing scope at definition time, which is why they're commonly used for callbacks inside class methods orsetTimeoutto avoid losing context.const obj = { name: 'A', regular() { return this.name; }, arrow: () => this?.name, }; -
How does prototypal inheritance work under the hood?
Every object has an internal
[[Prototype]]link (exposed via__proto__orObject.getPrototypeOf) to another object. Property lookups walk this prototype chain until the property is found or the chain ends atnull.classsyntax is sugar over this: a class'sprototypeobject holds shared methods, and instances link to it.function Animal(name) { this.name = name; } Animal.prototype.speak = function () { return `${this.name} makes a sound`; }; const dog = new Animal('Rex'); dog.speak(); // found via prototype chainUnlike classical inheritance, this is dynamic — mutating a prototype at runtime affects all objects linked to it.
Object.create(proto)lets you build a prototype chain explicitly without a constructor. -
What's the real difference between `var`, `let`, and `const`?
varis function-scoped (or globally scoped), hoisted and initialized toundefined, and allows redeclaration — this makes it leak out of blocks likeiforfor.letandconstare block-scoped, hoisted but left in the temporal dead zone until their declaration line executes (accessing them earlier throws aReferenceError), and cannot be redeclared in the same scope.constadditionally prevents reassignment of the binding — but the value itself can still be mutated if it's an object or array.const arr = [1, 2]; arr.push(3); // fine, mutating contents arr = [4]; // TypeError, reassigning bindingIn modern code,
constis the default,letfor reassignment, andvaris essentially legacy. -
Compare Promises and async/await — what's actually different?
async/awaitis syntactic sugar over Promises, not a different concurrency model — under the hood, anasyncfunction still returns a Promise andawaitstill suspends via the microtask queue. The practical difference is readability and control flow:async/awaitlets you write asynchronous logic that looks synchronous, withtry/catchfor errors instead of.catch()chains, which is especially helpful for conditional or sequential async logic.// Promise chain fetchUser().then(u => fetchPosts(u.id)).catch(handleErr); // async/await try { const user = await fetchUser(); const posts = await fetchPosts(user.id); } catch (err) { handleErr(err); }One gotcha: sequential
awaitcalls run serially even when independent — usePromise.all([p1, p2])to parallelize instead of awaiting each in turn. -
What is hoisting, and how does it differ for function declarations, `var`, and `let`/`const`?
Hoisting is JavaScript's behavior of processing declarations before executing code in a scope. Function declarations are hoisted fully — both name and body — so they can be called before their textual position.
vardeclarations are hoisted but only the declaration, initialized toundefined, so reading it early givesundefinedrather than an error.letandconstare hoisted too, but stay uninitialized in the temporal dead zone until their line runs, so accessing them early throws.console.log(foo); // undefined var foo = 1; console.log(bar); // ReferenceError let bar = 2;Function expressions and arrow functions assigned to
var/letfollow their variable's hoisting rules, not the function-declaration rule. -
What's the difference between debounce and throttle, and when do you use each?
Both limit how often a function runs in response to frequent events, but with different strategies. Debounce delays execution until a pause in events — each new call resets the timer — so the function fires once after activity stops. It's ideal for search-input autocomplete or resize-end handlers. Throttle guarantees execution at most once per fixed interval regardless of how many events fire, useful for scroll or mousemove handlers where you need steady, periodic updates rather than a final one.
function debounce(fn, delay) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), delay); }; }Rule of thumb: debounce for "wait until they're done," throttle for "keep me updated periodically."
-
Explain shallow copy vs deep copy, with examples of pitfalls.
A shallow copy duplicates only the top-level structure; nested objects/arrays are still shared by reference.
{...obj},Object.assign({}, obj), andArray.prototype.slice()are all shallow. A deep copy recursively duplicates every nested level so the copy is fully independent.const original = { a: 1, nested: { b: 2 } }; const shallow = { ...original }; shallow.nested.b = 99; console.log(original.nested.b); // 99 — shared reference! const deep = structuredClone(original); // true deep copyThe classic pitfall is assuming spread/
Object.assignfully isolates state, then mutating a nested object and unexpectedly affecting the "original" — a common source of React state bugs.structuredClone()(or historicallyJSON.parse(JSON.stringify()), which loses functions/Date/undefined) is used for real deep copies; libraries like Lodash'scloneDeephandle edge cases more robustly. -
Why does JavaScript have both `==` and `===`, and when would you ever use `==`?
===(strict equality) compares value and type with no conversion.==(loose equality) performs type coercion before comparing, following rules that are notoriously inconsistent ('' == 0istrue,null == undefinedistrue, butnull == 0isfalse). Best practice is to default to===everywhere to avoid surprising coercions.0 == '0'; // true 0 == []; // true '0' == [0]; // true (!!) null == undefined; // true null === undefined; // falseThe one broadly accepted exception is
x == null, which cleanly checks for bothnullandundefinedin a single comparison without an explicit OR — some style guides permit this specific idiom, but otherwise==is considered a footgun best avoided. -
What are higher-order functions, and why are they central to idiomatic JS?
A higher-order function either takes a function as an argument, returns a function, or both. They enable composition and let you abstract control flow instead of writing it manually. Array methods like
map,filter,reduce, andsortare higher-order functions; so aredebounce, event handler factories, and middleware patterns (Express, Redux).const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x); const double = x => x * 2; const inc = x => x + 1; compose(double, inc)(5); // double(inc(5)) = 12They're preferred over manual
forloops for transformations because they communicate intent declaratively, are easier to test in isolation, and compose well — though excessive chaining can hurt readability and performance on very large datasets compared to a single imperative pass. -
How does the ESM module system differ from CommonJS, and why does it matter in practice?
CommonJS (
require/module.exports) loads modules synchronously at runtime, resolves and executes the entire file whenrequire()is called, and exports are a mutable object — so consumers get a live reference but the resolution happens eagerly and can't be statically analyzed. ESM (import/export) is parsed statically before execution, enabling tree-shaking, live bindings (imported values reflect updates in the source module), and native async loading in browsers.// CommonJS const { readFile } = require('fs'); module.exports = { foo }; // ESM import { readFile } from 'fs'; export const foo = 1;Practically, this matters because bundlers can eliminate unused ESM exports but not CommonJS ones, and Node.js requires explicit configuration (
"type": "module",.mjs, or.cjs) to know which system a file uses, since interop between the two has edge cases (default export shape, top-levelawaitonly in ESM).
Practicing for a real interview? Get live AI help with your answers, in the browser, free to start.
Start a free session