JavaScript Interview Prep

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.

  1. 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(); // 2

    count isn't accessible from outside, but the returned function still reads and mutates it. Closures are also the classic source of the "loop variable" bug when var is used instead of let inside setTimeout callbacks.

  2. 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/await continuations) before touching the macrotask queue (setTimeout, setInterval, I/O, UI rendering, setImmediate in 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, 2

    Microtasks 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.

  3. How does `this` get determined in JavaScript?

    this is determined by how a function is called, not where it's defined (except for arrow functions). The rules, in priority order: (1) new Foo()this is the newly created object; (2) fn.call(obj)/.apply(obj)/.bind(obj)this is explicitly set; (3) obj.method()this is obj (implicit binding); (4) plain function call — this is undefined in strict mode, or the global object otherwise. Arrow functions don't have their own this; they lexically inherit it from the enclosing scope at definition time, which is why they're commonly used for callbacks inside class methods or setTimeout to avoid losing context.

    const obj = {
      name: 'A',
      regular() { return this.name; },
      arrow: () => this?.name,
    };
  4. How does prototypal inheritance work under the hood?

    Every object has an internal [[Prototype]] link (exposed via __proto__ or Object.getPrototypeOf) to another object. Property lookups walk this prototype chain until the property is found or the chain ends at null. class syntax is sugar over this: a class's prototype object 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 chain

    Unlike 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.

  5. What's the real difference between `var`, `let`, and `const`?

    var is function-scoped (or globally scoped), hoisted and initialized to undefined, and allows redeclaration — this makes it leak out of blocks like if or for. let and const are block-scoped, hoisted but left in the temporal dead zone until their declaration line executes (accessing them earlier throws a ReferenceError), and cannot be redeclared in the same scope. const additionally 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 binding

    In modern code, const is the default, let for reassignment, and var is essentially legacy.

  6. Compare Promises and async/await — what's actually different?

    async/await is syntactic sugar over Promises, not a different concurrency model — under the hood, an async function still returns a Promise and await still suspends via the microtask queue. The practical difference is readability and control flow: async/await lets you write asynchronous logic that looks synchronous, with try/catch for 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 await calls run serially even when independent — use Promise.all([p1, p2]) to parallelize instead of awaiting each in turn.

  7. 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. var declarations are hoisted but only the declaration, initialized to undefined, so reading it early gives undefined rather than an error. let and const are 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/let follow their variable's hoisting rules, not the function-declaration rule.

  8. 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."

  9. 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), and Array.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 copy

    The classic pitfall is assuming spread/Object.assign fully isolates state, then mutating a nested object and unexpectedly affecting the "original" — a common source of React state bugs. structuredClone() (or historically JSON.parse(JSON.stringify()), which loses functions/Date/undefined) is used for real deep copies; libraries like Lodash's cloneDeep handle edge cases more robustly.

  10. 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 ('' == 0 is true, null == undefined is true, but null == 0 is false). Best practice is to default to === everywhere to avoid surprising coercions.

    0 == '0';        // true
    0 == [];         // true
    '0' == [0];      // true (!!)
    null == undefined; // true
    null === undefined; // false

    The one broadly accepted exception is x == null, which cleanly checks for both null and undefined in a single comparison without an explicit OR — some style guides permit this specific idiom, but otherwise == is considered a footgun best avoided.

  11. 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, and sort are higher-order functions; so are debounce, 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)) = 12

    They're preferred over manual for loops 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.

  12. 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 when require() 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-level await only in ESM).

Practicing for a real interview? Get live AI help with your answers, in the browser, free to start.

Start a free session