Ch. 01

JavaScript

The language every web interview starts with: scope, closures, this, prototypes, async code and the event loop.

57 interview questions42 quiz questions7 notes14 coding challenges
your progress0%

Notes in this chapter

Filter all notes →
read ✓JavaScript · mid

Closures, Scope & the Classic setTimeout Loop

What lexical scope and closures really are, why the var + setTimeout loop prints 3 3 3, three ways to fix it, and where closures earn their keep in real code.

~6 min readread →
read ✓JavaScript · hard

The Event Loop: Microtasks vs Macrotasks

How the call stack, task queue and microtask queue fit together, where rendering happens, how async/await schedules work, and output puzzles with answers.

~7 min readread →
read ✓JavaScript · easy

Hoisting, the TDZ and var vs let vs const

What the engine sets up before your code runs, why var reads as undefined while let throws, how the temporal dead zone works, and why const doesn't mean immutable.

~6 min readread →
read ✓JavaScript · mid

Prototypes & Inheritance Without the Headache

How the prototype chain resolves properties, what __proto__ and prototype each mean, what new and class do under the hood, and how to wire up inheritance by hand.

~7 min readread →
read ✓JavaScript · mid

Implement Debounce and Throttle From Scratch

Build debounce and throttle the way interviewers expect: timelines, this and args, cancel(), leading and trailing edges, and how to test them.

~7 min readread →
read ✓JavaScript · hard

Write Promise.all (and Friends) From Scratch

Implement Promise.all with native semantics, then allSettled, race, any and a concurrency-limited pool, plus the edge cases interviewers probe.

~6 min readread →

Top 57 JavaScript interview questions most asked first

  1. 1.What is a closure in JavaScript? Explain it with a practical example.easy

    A closure is a function bundled together with its lexical environment: the variables that were in scope where the function was defined. When an inner function is returned or passed elsewhere, it keeps access to those outer variables even after the outer function has returned, because the engine keeps them alive as long as something can still reach them.

    In the example, counter() returns an arrow function that closes over n. Each call to counter() creates a fresh n, so the counters are independent and nothing outside can modify n directly.

    Practical uses:

    • Private state and encapsulation (the module pattern)
    • Function factories and partial application
    • Memoization caches, once(), debounce and throttle timers
    • Event handlers and callbacks that remember their context

    The classic gotcha: var in a for loop with setTimeout logs the final value every time, because all callbacks share one function-scoped variable; let creates a new binding per iteration. Closures can also keep large objects in memory longer than you intended.

    function counter() {
      let n = 0; // private: only the returned function can reach it
      return () => ++n;
    }
    
    const a = counter();
    const b = counter();
    a(); a(); // 1, 2
    b();      // 1 (b has its own n)
    What interviewers listen for
    • Function plus the lexical environment where it was defined
    • Outer variables stay alive after the outer function returns
    • Each call of the outer function creates a fresh scope
    • Uses: private state, factories, memoization, once()
    • var-in-loop gotcha; can retain memory unintentionally

    Likely follow-up: How would you implement once(fn) using a closure? · Why does a var loop with setTimeout log 3, 3, 3?

  2. 2.What are the differences between var, let and const?easy

    They differ in scope, hoisting, redeclaration and reassignment:

    • var is function-scoped (it ignores blocks like if and for), is hoisted and initialized to undefined, can be redeclared, and a top-level var in a classic browser script becomes a property of window.
    • let is block-scoped, is hoisted but not initialized (reading it before its declaration throws a ReferenceError, the temporal dead zone), can't be redeclared in the same scope, but can be reassigned.
    • const behaves like let but must be initialized and can't be reassigned. It does not make the value immutable: you can still mutate a const object or array. Use Object.freeze for that.

    In a for loop, let creates a fresh binding per iteration, which fixes the classic closure-in-a-loop bug.

    My default is const everywhere, let only when I need to reassign, and no var in modern code.

    What interviewers listen for
    • var is function-scoped; let/const are block-scoped
    • let/const sit in the TDZ until their declaration runs
    • var can be redeclared; let/const cannot
    • const prevents reassignment, not mutation
    • Prefer const, then let; avoid var

    Likely follow-up: What is the temporal dead zone? · Why does let fix the setTimeout-in-a-loop bug?

  3. 3.What is hoisting, and what is the temporal dead zone (TDZ)?mid

    Hoisting is the effect of JavaScript creating all the bindings of a scope before it runs any code in that scope, so declarations behave as if they were moved to the top. What differs is how each kind is initialized:

    • Function declarations are hoisted together with their body, so you can call them before the line where they appear.
    • var is hoisted and initialized to undefined, so reading it early gives undefined rather than an error.
    • let, const and class are hoisted but left uninitialized. From the start of the scope until the declaration runs, the binding is in the temporal dead zone, and any access, even typeof, throws a ReferenceError.
    • Function expressions follow the rules of their variable: calling var fn = function () {} too early throws TypeError: fn is not a function.

    The TDZ exists to surface use-before-initialization bugs instead of silently handing you undefined.

    console.log(sayHi()); // "hi": declaration hoisted with its body
    console.log(x);       // undefined: var hoisted and initialized
    console.log(y);       // ReferenceError: Cannot access 'y' before initialization
    
    function sayHi() { return 'hi'; }
    var x = 1;
    let y = 2;
    What interviewers listen for
    • Bindings are created before the scope's code runs
    • Function declarations are hoisted with their body
    • var is initialized to undefined
    • let/const/class are uninitialized: access throws ReferenceError
    • typeof is not safe inside the TDZ

    Likely follow-up: Are classes hoisted? · What does typeof return for an undeclared variable versus one in the TDZ?

  4. 4.What is the difference between == and ===? Give some surprising coercion examples.easy

    === is strict equality: it's true only when both operands have the same type and the same value, with no conversion. == is loose equality: when the types differ it coerces the operands first, following the Abstract Equality rules, and that produces surprises:

    • '' == 0, '0' == false and [] == false are all true, because both sides end up converted to numbers.
    • null == undefined is true, but null == 0 is false: null and undefined only loosely equal each other.
    • NaN is not equal to anything, including itself, with either operator.
    • Objects are compared by reference with both: {} === {} is false.

    In practice I use === everywhere. The one idiom some teams allow is x == null, which checks for null and undefined in one go. For edge cases there's Object.is, which treats NaN as equal to itself and distinguishes +0 from -0.

    What interviewers listen for
    • === compares type and value without coercion
    • == coerces operands, usually toward numbers
    • null == undefined is true, but null == 0 is false
    • NaN never equals itself; use Number.isNaN
    • Object.is handles NaN and -0

    Likely follow-up: Why is [] == ![] true?

  5. 5.Explain the event loop. How does single-threaded JavaScript handle asynchronous work?mid

    JavaScript runs your code on a single thread with one call stack. Slow work such as timers, network requests and I/O is handed to the host environment (the browser's Web APIs, or libuv in Node), which does it outside the stack and queues a callback when it finishes.

    The event loop coordinates the queues:

    • Run the current task until the call stack is empty.
    • Drain the microtask queue completely: promise reactions (then/catch/finally, code after await), queueMicrotask and MutationObserver callbacks.
    • In browsers, render if needed; requestAnimationFrame callbacks run just before style, layout and paint.
    • Pick the next macrotask (a timer callback, a UI event, a message) and repeat.

    That's why the snippet logs the synchronous lines first, then the promise, then the timeout, even with a 0 ms delay. And because everything shares one thread, a long synchronous loop blocks clicks, timers and rendering.

    console.log('start');
    setTimeout(() => console.log('timeout'), 0);
    Promise.resolve().then(() => console.log('promise'));
    console.log('end');
    // start, end, promise, timeout
    What interviewers listen for
    • One thread and one call stack; the host does async work
    • Microtask queue is drained fully after each task
    • Promises are microtasks; timers and events are macrotasks
    • Rendering happens between tasks, so long tasks freeze the UI

    Likely follow-up: What happens if a microtask keeps scheduling another microtask? · Is setTimeout(fn, 0) really zero milliseconds?

  6. 6.How is the value of this determined in JavaScript?mid

    For regular functions, this is decided by how the function is called, not where it's defined. The rules, from highest to lowest precedence:

    • new binding: new Foo() sets this to the newly created object.
    • Explicit binding: fn.call(obj), fn.apply(obj) or a function made with fn.bind(obj) use obj.
    • Implicit binding: obj.method() uses the object before the dot.
    • Default binding: a plain call fn() gets undefined in strict mode (which includes modules and class bodies), or the global object in sloppy mode.

    Arrow functions ignore all of these: they have no this of their own and use the this of the enclosing scope.

    The classic bug is losing the receiver: const greet = user.greet; greet(), or passing user.greet as a callback, turns an implicit call into a default one. Fix it with bind, a wrapping arrow function, or a class field holding an arrow function.

    'use strict';
    const user = {
      name: 'Ada',
      greet() { return this?.name; },
    };
    
    user.greet();                     // "Ada" (implicit binding)
    const greet = user.greet;
    greet();                          // undefined (default binding, strict mode)
    user.greet.call({ name: 'Bob' }); // "Bob" (explicit binding)
    What interviewers listen for
    • Regular functions: decided by the call site
    • Precedence: new, then call/apply/bind, then method call, then default
    • Default is undefined in strict mode, global object otherwise
    • Arrow functions take this from the enclosing scope
    • Extracting a method loses its this

    Likely follow-up: Why does this break when you pass a class method as an event handler? · Can you change the this of a bound function with call?

  7. 7.What is a Promise? Explain its states and how chaining works.mid

    A Promise is an object representing the eventual result of an asynchronous operation. It is in one of three states: pending, fulfilled (with a value) or rejected (with a reason). Once fulfilled or rejected it is settled and can never change again; later resolve or reject calls are ignored.

    Key behaviors:

    • The executor passed to new Promise(...) runs synchronously; only the reactions are asynchronous.
    • then, catch and finally callbacks always run as microtasks, even if the promise is already settled.
    • then returns a new promise, resolved with whatever the callback returns. Returning a promise makes the chain wait for it; throwing rejects it.
    • A rejection skips then handlers until a catch handles it, and the chain continues normally after it recovers.

    That's what gives you a flat chain instead of nested callbacks, with one catch at the end handling errors from every step.

    What interviewers listen for
    • Three states: pending, fulfilled, rejected; settles only once
    • Executor runs synchronously; handlers run as microtasks
    • then returns a new promise, enabling chaining
    • Returning a promise waits for it; throwing rejects
    • Errors propagate down the chain to the nearest catch

    Likely follow-up: What happens to a rejected promise that nobody handles? · How would you wrap a callback-based API in a promise?

  8. 8.How do arrow functions differ from regular functions?easy

    Arrow functions are more than shorter syntax; they behave differently:

    • No own this: they capture this from the surrounding scope, and call, apply and bind can't change it. Perfect for callbacks inside methods, wrong for object methods that need this.
    • No own arguments: use rest parameters (...args) instead.
    • Not constructors: new on an arrow function throws a TypeError, and they have no prototype property.
    • No super or new.target of their own, and they can't be generators.
    • Concise bodies return implicitly: x => x * 2. To return an object literal, wrap it in parentheses: () => ({ a: 1 }).

    Regular functions get a dynamic this from the call site and can be used with new. Function declarations are also hoisted, whereas arrow functions are always expressions, so they follow the hoisting rules of the variable they're assigned to.

    What interviewers listen for
    • Lexical this, not changeable by call/apply/bind
    • No arguments object; use rest parameters
    • Can't be used with new; no prototype
    • Implicit return; wrap object literals in parentheses
    • Avoid them as object methods that need this

    Likely follow-up: When should you not use an arrow function?

  9. 9.What is the difference between call, apply and bind?mid

    All three set this explicitly for a regular function:

    • fn.call(thisArg, a, b) invokes the function immediately, with the arguments listed individually.
    • fn.apply(thisArg, [a, b]) invokes it immediately, with the arguments as an array or array-like. Mnemonic: A for array. Before spread syntax, Math.max.apply(null, nums) was the idiom.
    • fn.bind(thisArg, a) does not call the function. It returns a new bound function with this fixed and, optionally, some leading arguments pre-filled (partial application).

    A bound function's this is permanent: call, apply or binding it again won't change it. The exception is new: using a bound function as a constructor ignores the bound this and creates a fresh object.

    Typical uses are borrowing methods (Array.prototype.slice.call(arguments)), fixing this for callbacks and event handlers, and partial application. None of them can change an arrow function's this.

    function greet(greeting, punct) {
      return `${greeting}, ${this.name}${punct}`;
    }
    const ann = { name: 'Ann' };
    
    greet.call(ann, 'Hi', '!');       // "Hi, Ann!"
    greet.apply(ann, ['Hi', '!']);    // "Hi, Ann!"
    const hiAnn = greet.bind(ann, 'Hi');
    hiAnn('?');                       // "Hi, Ann?"
    hiAnn.call({ name: 'Zed' }, '.'); // "Hi, Ann." (bound this wins)
    What interviewers listen for
    • call: invoke now, arguments listed individually
    • apply: invoke now, arguments as an array
    • bind: returns a new function with this fixed
    • Bound this can't be overridden, except by new
    • Used for method borrowing and partial application

    Likely follow-up: Write a simple polyfill for Function.prototype.bind.

  10. 10.How does prototypal inheritance work in JavaScript?mid

    Every object has an internal [[Prototype]] link to another object or to null. When you read a property, the engine checks the object itself, then follows the link to its prototype, then that object's prototype, and so on until it finds the property or reaches null and returns undefined. That's the prototype chain. Assigning a property normally creates it on the object itself, shadowing any inherited one.

    Two things are often confused:

    • Object.getPrototypeOf(obj) (or the legacy obj.__proto__) is an object's actual prototype.
    • Fn.prototype is a property of functions: the object that becomes the prototype of instances created with new Fn().

    So methods defined on Person.prototype are shared by every instance rather than copied into each one. You can set up inheritance with Object.create(proto), constructor functions, or class ... extends, which is syntax over the same mechanism. Built-ins work the same way: [].map comes from Array.prototype.

    const animal = {
      speak() { return `${this.name} makes a sound`; },
    };
    const dog = Object.create(animal); // dog's prototype is animal
    dog.name = 'Rex';
    
    dog.speak();                             // "Rex makes a sound"
    dog.hasOwnProperty('speak');             // false: inherited
    Object.getPrototypeOf(dog) === animal;   // true
    Object.getPrototypeOf(Object.prototype); // null: end of the chain
    What interviewers listen for
    • Objects link to a prototype through [[Prototype]]
    • Property lookup walks the chain until null
    • __proto__ is the link; Fn.prototype is for instances
    • Methods on the prototype are shared, not copied
    • class is syntax over prototypes

    Likely follow-up: What is the difference between __proto__ and prototype? · How do you create an object with no prototype at all?

  11. 11.How does async/await work, and how does it relate to promises?mid

    async/await is syntax built on top of promises that lets asynchronous code read like synchronous code.

    • An async function always returns a promise: returning a value fulfills it, throwing rejects it.
    • await pauses that function until the promise settles, then resumes with the value or throws the rejection reason, so ordinary try/catch works. Awaiting a non-promise simply wraps it in a resolved promise.
    • Pausing doesn't block the thread: the rest of the function is resumed as a microtask once the promise settles, and other code runs meanwhile. The function runs synchronously until its first await.

    Common mistakes:

    • Accidental serialization: awaiting independent calls one after another. Start them together with await Promise.all([...]).
    • await inside forEach doesn't wait; forEach ignores the returned promises. Use for...of for sequential work or Promise.all with map for parallel work.
    • Forgetting await, so a rejection escapes your try/catch.

    await is allowed inside async functions and at the top level of ES modules.

    // Assume each call takes about 1 second
    const user = await getUser();          // sequential: ~2s total
    const posts = await getPosts();
    
    // Parallel: ~1s total
    const [user2, posts2] = await Promise.all([getUser(), getPosts()]);
    
    // Does NOT wait for the saves:
    ids.forEach(async (id) => await save(id));
    // Waits for each one, in order:
    for (const id of ids) await save(id);
    What interviewers listen for
    • An async function always returns a promise
    • await pauses the function, not the thread
    • Handle rejections with try/catch
    • Run independent work in parallel with Promise.all
    • await inside forEach does not wait

    Likely follow-up: How do you handle errors from several parallel awaits? · What happens if you forget await inside a try block?

  12. 12.What does this code log, and why?hard

    It logs 1 6 8 3 5 7 4 2.

    • Synchronous code runs first: 1, then the async IIFE runs synchronously up to its first await and logs 6, then 8.
    • At that point the microtask queue holds, in order: the first then callback, the queueMicrotask callback, and the continuation after await null. Draining it logs 3, 5, 7.
    • Running the 3 callback fulfilled the promise returned by the first then, which queued the second then callback at the end of the queue. It runs in the same drain: 4.
    • Only when the microtask queue is empty does the event loop take the next macrotask, the timer: 2.

    The takeaways: an async function is synchronous until its first await; microtasks run in FIFO order, and microtasks queued during the drain still run before any macrotask; setTimeout(fn, 0) always waits for the whole microtask queue to empty.

    console.log('1');
    setTimeout(() => console.log('2'), 0);
    Promise.resolve()
      .then(() => console.log('3'))
      .then(() => console.log('4'));
    queueMicrotask(() => console.log('5'));
    (async () => {
      console.log('6');
      await null;
      console.log('7');
    })();
    console.log('8');
    What interviewers listen for
    • Synchronous code first, including async functions up to await
    • Microtasks run FIFO: then, queueMicrotask, await continuations
    • Chained then is queued only when the previous one finishes
    • Timers are macrotasks and run after all microtasks

    Likely follow-up: Where would a setTimeout scheduled inside the 3 callback run? · What changes if the IIFE awaited a promise that resolves after 100 ms?

  13. 13.What is the difference between debouncing and throttling? When would you use each?mid

    Both limit how often a function runs during a burst of events, but in different ways:

    • Debounce waits until the events stop. Every call resets a timer, and the function runs only after delay ms of silence, so 50 quick keystrokes produce one call at the end. Use it for search-as-you-type requests, autosave, input validation, or recalculating after a window resize finishes.
    • Throttle runs the function at most once per interval while events keep coming. A two-second scroll with a 200 ms throttle runs roughly ten times. Use it for scroll and pointer-move handlers, infinite-scroll checks, or rate-limiting repeated clicks.

    Details interviewers probe: preserving this and the arguments with fn.apply(this, args), clearing the old timer, leading versus trailing execution (a throttle that drops the last event can leave the UI stale), and a cancel() method for cleanup when a component unmounts. For purely visual updates, requestAnimationFrame is often a better throttle.

    function debounce(fn, delay) {
      let timer;
      return function (...args) {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), delay);
      };
    }
    
    const onSearch = debounce((q) => fetchResults(q), 300);
    input.addEventListener('input', (e) => onSearch(e.target.value));
    What interviewers listen for
    • Debounce: run once events stop for delay ms
    • Throttle: run at most once per interval
    • Debounce: search input, autosave, resize end
    • Throttle: scroll, mousemove, rate limiting
    • Preserve this and args; leading/trailing; cancel

    Likely follow-up: Implement throttle so the last call in a burst isn't lost. · How would you add cancel() and flush() to your debounce?

  14. 14.Explain event bubbling, event capturing and event delegation.mid

    When an event fires on a DOM element, it travels in three phases:

    • Capturing: from window down through the ancestors toward the target.
    • Target: listeners on the element itself run.
    • Bubbling: back up from the target through each ancestor to window.

    addEventListener listeners run in the bubbling phase by default; pass { capture: true } (or true) to run during capturing instead. Most events bubble, but some don't, such as focus, blur, mouseenter and mouseleave (focusin and focusout are the bubbling alternatives).

    Event delegation takes advantage of bubbling: instead of attaching a listener to every child, attach one to a common ancestor and inspect event.target to find what was actually clicked, typically with event.target.closest(selector). event.currentTarget is the element the listener is attached to.

    The benefits are far fewer listeners and automatic support for children added later, which is ideal for lists, tables and menus. closest also handles clicks on nested elements inside an item, such as an icon inside a button.

    const list = document.querySelector('#todo-list');
    
    list.addEventListener('click', (event) => {
      const button = event.target.closest('button.delete');
      if (!button || !list.contains(button)) return; // clicked something else
      button.closest('li').remove();
    });
    // Also works for items added to the list later.
    What interviewers listen for
    • Three phases: capture down, target, bubble up
    • Listeners bubble by default; { capture: true } for capturing
    • Delegation: one listener on an ancestor, inspect event.target
    • target is the origin; currentTarget has the listener
    • Handles dynamic children; not every event bubbles

    Likely follow-up: What is the difference between event.target and event.currentTarget?

  15. 15.What is the difference between primitive and reference types? Is JavaScript pass-by-value or pass-by-reference?easy

    JavaScript has seven primitive types: string, number, bigint, boolean, undefined, symbol and null. Primitives are immutable and are compared by value: assigning one copies the value, and methods like toUpperCase() return a new string rather than changing the original.

    Everything else is an object (including arrays, functions, dates and maps). A variable holding an object holds a reference to it, so assigning or passing it copies the reference, and both names point at the same object. Mutating through either one is visible through both, and === compares identity: two objects with identical contents are not equal.

    Strictly speaking, JavaScript is always pass-by-value; for objects, the value being passed is a reference (sometimes called "call by sharing"). That's why a function can mutate an object you pass in, but reassigning the parameter has no effect on the caller's variable.

    This matters for React state, default parameters and caches: copy objects instead of mutating shared ones.

    What interviewers listen for
    • Seven primitives; everything else is an object
    • Primitives are immutable and compared by value
    • Objects are held and compared by reference
    • Always pass-by-value; for objects, the value is a reference
    • Mutating a parameter is visible; reassigning it is not

    Likely follow-up: How would you compare two objects by their contents?

  16. 16.What is the difference between a shallow copy and a deep copy? How would you deep clone an object?mid

    A shallow copy creates a new top-level object but copies nested values by reference, so nested objects are still shared with the original. Spread ({ ...obj }, [...arr]), Object.assign, Array.from and slice all make shallow copies. A deep copy recursively copies every nested object, so the two are completely independent.

    Options for deep copying:

    • structuredClone(value) is the built-in choice. It handles nested objects and arrays, Date, Map, Set, RegExp, typed arrays and even circular references. It throws a DataCloneError for functions and DOM nodes, and class instances come back as plain objects, because prototypes aren't preserved.
    • JSON.parse(JSON.stringify(value)) is the old trick, but lossy: functions, undefined and symbols disappear, a Date becomes a string, Map and Set become {}, NaN and Infinity become null, and circular references or BigInt values throw.
    • A library such as Lodash's cloneDeep, or a recursive function when you need custom behavior.
    const original = { user: { name: 'Ada' }, when: new Date(0) };
    
    const shallow = { ...original };
    shallow.user.name = 'Grace';
    console.log(original.user.name);        // "Grace": nested object is shared
    
    const deep = structuredClone(original);
    deep.user.name = 'Linus';
    console.log(original.user.name);        // "Grace": fully independent
    console.log(deep.when instanceof Date); // true
    What interviewers listen for
    • Shallow: new top level, nested objects still shared
    • Spread, Object.assign and slice are shallow
    • structuredClone handles Date, Map, Set and cycles
    • JSON round-trip loses functions, undefined, Dates, Map/Set
    • structuredClone throws on functions and drops prototypes

    Likely follow-up: Write a recursive deep clone that handles circular references.

  17. 17.What is the difference between null and undefined?easy

    Both mean "no value", but they signal different things:

    • undefined means a value hasn't been assigned. You get it from a declared but uninitialized variable, a missing object property, a missing function argument, or a function without a return.
    • null is an intentional empty value that code assigns explicitly, for example "this user has no manager".

    Practical differences:

    • typeof undefined is "undefined", but typeof null is "object", a historical bug kept for compatibility.
    • null == undefined is true, but null === undefined is false.
    • In arithmetic, null converts to 0 and undefined to NaN.
    • Default parameters and destructuring defaults kick in only for undefined, not for null.
    • JSON.stringify drops properties whose value is undefined but keeps null.

    To handle both at once, use ??, ?. or x == null.

    What interviewers listen for
    • undefined: not assigned; null: intentionally empty
    • typeof null is "object", a legacy bug
    • Loosely equal, but not strictly equal
    • Defaults apply only for undefined
    • JSON keeps null and drops undefined properties

    Likely follow-up: How do you reliably check for null or undefined in one condition?

  18. 18.Explain map, filter and reduce. When would you use each one?easy

    All three are array methods that take a callback, return something new without mutating the original array, and let you describe transformations declaratively:

    • map(fn) returns a new array of the same length, where each element is fn(item, index, array). Use it to transform: prices into formatted strings, API rows into view models.
    • filter(fn) returns a new array with only the elements for which fn returns a truthy value. Use it to select: active users, items matching a search.
    • reduce(fn, initial) walks the array carrying an accumulator and returns a single value, which can be a number, an object or another array. Use it for sums, counts, grouping or building a lookup by id.

    Gotchas: always pass an initial value to reduce, because calling it on an empty array without one throws a TypeError. ['1', '2', '3'].map(parseInt) returns [1, NaN, NaN], because map passes the index as parseInt's radix. Chaining is readable but loops several times; on hot paths a single loop can be faster.

    const orders = [
      { total: 40, paid: true },
      { total: 15, paid: false },
      { total: 25, paid: true },
    ];
    
    const revenue = orders
      .filter((o) => o.paid)           // the two paid orders
      .map((o) => o.total)             // [40, 25]
      .reduce((sum, t) => sum + t, 0); // 65
    What interviewers listen for
    • map: transform each item, same length
    • filter: keep items that pass a predicate
    • reduce: fold into one value with an accumulator
    • None of them mutate the original array
    • Always give reduce an initial value

    Likely follow-up: Implement map and filter using reduce. · Why does ['1', '2', '3'].map(parseInt) return [1, NaN, NaN]?

  19. 19.Compare Promise.all, Promise.allSettled, Promise.race and Promise.any.mid

    All four take an iterable of promises (plain values count as already fulfilled) and return one promise:

    • Promise.all fulfills with an array of every value, in input order, once all of them fulfill. It rejects as soon as any one rejects, with that reason. Use it for independent requests you need together.
    • Promise.allSettled waits for all of them and never rejects. It returns objects shaped like { status: 'fulfilled', value } or { status: 'rejected', reason }. Use it when partial failure is fine, such as a dashboard of independent widgets.
    • Promise.race settles like the first promise to settle, whether it fulfills or rejects. The classic use is a timeout.
    • Promise.any fulfills with the first fulfilled value and ignores rejections. If every promise rejects, it rejects with an AggregateError whose errors array holds the reasons. Use it for fallbacks and redundant sources.

    None of them cancel the losing promises; that work keeps running unless you abort it, e.g. with an AbortController. Edge cases: Promise.all([]) fulfills with [], while Promise.race([]) stays pending forever.

    const timeout = (ms) =>
      new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms));
    
    // Fail if the request takes longer than 5 seconds
    const res = await Promise.race([fetch('/api/data'), timeout(5000)]);
    
    // Load everything, even if some requests fail
    const results = await Promise.allSettled(urls.map((u) => fetch(u)));
    const ok = results.filter((r) => r.status === 'fulfilled').map((r) => r.value);
    What interviewers listen for
    • all: every value, rejects on the first rejection
    • allSettled: never rejects, reports each outcome
    • race: settles like the first to settle
    • any: first fulfillment; AggregateError if all reject
    • None of them cancel the remaining promises

    Likely follow-up: Implement Promise.all from scratch. · How would you run 100 requests with at most 5 in flight?

  20. 20.What is the difference between the spread operator and rest parameters?easy

    They share the ... syntax but do opposite jobs:

    • Spread expands an iterable or object into individual elements or properties: fn(...args), [...a, ...b], { ...defaults, ...options }. Array spread works with any iterable (strings, Set, Map), while object spread copies an object's own enumerable properties, with later keys overwriting earlier ones.
    • Rest collects several elements into one array or object. It's used in parameter lists (function log(level, ...messages)) and in destructuring (const [first, ...others] = arr, const { id, ...props } = obj). It must come last, otherwise it's a SyntaxError.

    Rule of thumb: in a call or on the right-hand side of an assignment, it's spread; in a parameter list or a destructuring pattern, it's rest.

    Both produce shallow copies. Rest parameters give you a real array, unlike the old arguments object, which is only array-like and doesn't exist in arrow functions. One edge case: { ...null } is fine and gives {}, but [...null] throws a TypeError.

    What interviewers listen for
    • Spread expands; rest collects
    • Rest must be last, and it is a real array
    • Object spread copies own enumerable properties; later keys win
    • Both make shallow copies
    • Rest parameters replace the arguments object

    Likely follow-up: How would you remove a property from an object without mutating it?

  21. 21.What is scope in JavaScript, and how does the scope chain work?easy

    Scope determines where a variable is visible. JavaScript has:

    • Global scope: visible everywhere.
    • Module scope: top-level variables in an ES module are private to that module.
    • Function scope: variables declared inside a function, including var.
    • Block scope: let, const and class declared inside { }, such as an if or for block.

    JavaScript uses lexical (static) scoping: a function's scope is determined by where it is written, not where it is called. When you reference a variable, the engine looks in the current scope, then the enclosing one, and so on outward to the global scope. That lookup path is the scope chain. If the name isn't found, you get a ReferenceError; in sloppy mode, assigning to an undeclared name silently creates a global instead, which strict mode forbids.

    An inner variable with the same name shadows the outer one. Lexical scoping is also what makes closures possible: a function keeps its scope chain wherever it goes.

    What interviewers listen for
    • Global, module, function and block scope
    • Lexical: scope is fixed by where code is written
    • Lookup walks outward through the scope chain
    • Inner declarations shadow outer ones
    • Not found means ReferenceError; the basis of closures

    Likely follow-up: What is the difference between lexical and dynamic scope?

  22. 22.What is the difference between forEach and map?easy

    Both call a function once for each array element, but they serve different purposes:

    • map returns a new array of the callback's return values, with the same length as the original. It's for transforming data, and it's chainable: arr.map(...).filter(...).
    • forEach returns undefined. It's for side effects: logging, updating the DOM, writing into another structure. You can't chain anything after it.

    Using map only for side effects and ignoring the result is a code smell, and using forEach plus push to build a new array usually means you wanted map or filter.

    Shared limitations:

    • You can't break out of either one, except by throwing. For an early exit use for...of, some, every or find.
    • Neither waits for async callbacks: forEach ignores the returned promises, while map gives you an array of promises you can pass to Promise.all.
    • Both skip empty slots in sparse arrays.

    Neither mutates the array by itself, although the callback can.

    What interviewers listen for
    • map returns a new array; forEach returns undefined
    • map for transformations, forEach for side effects
    • Neither supports break; use for...of or some
    • Neither awaits async callbacks
    • map is chainable

    Likely follow-up: How would you stop iterating early?

  23. 23.What is destructuring? Show some useful patterns.easy

    Destructuring unpacks values from arrays, or properties from objects, into variables using a pattern that mirrors the data's shape.

    • Object destructuring matches by property name: const { name, age } = user. You can rename ({ name: userName }), set defaults ({ role = 'guest' }), nest ({ address: { city } }) and collect the remainder ({ id, ...others }).
    • Array destructuring matches by position: const [first, second] = list. You can skip items ([, second]), use defaults and collect the rest ([head, ...tail]). It works with any iterable, including strings and Map entries.

    Handy patterns:

    • Swapping without a temporary variable: [a, b] = [b, a].
    • Named options: function draw({ width = 100, color = 'red' } = {}), where the trailing = {} lets callers omit the argument entirely.
    • Unpacking tuples, such as const [count, setCount] = useState(0) in React.

    Gotchas: defaults apply only when the value is undefined, not null, and destructuring null or undefined throws a TypeError.

    What interviewers listen for
    • Objects match by name, arrays by position
    • Supports renaming, defaults, nesting and rest
    • Defaults apply only for undefined
    • Destructuring null or undefined throws a TypeError
    • Great for options objects and swapping values
  24. 24.What are truthy and falsy values in JavaScript?easy

    When a value is used in a boolean context, such as an if, a ternary, &&, || or !, it is converted to true or false. Only a handful of values are falsy:

    • false
    • 0, -0 and 0n
    • '' (the empty string)
    • null and undefined
    • NaN

    Everything else is truthy, including some surprising values: '0', 'false', ' ', [], {}, every function, and new Boolean(false), which is an object.

    Why it matters in practice:

    • if (items.length) is a common idiom, but if (items) is always true for an array, even an empty one.
    • count || 10 replaces a legitimate 0 with 10; use count ?? 10 when only null and undefined should fall back.
    • !!value or Boolean(value) converts explicitly.
    • Truthiness isn't the same as == true: [] is truthy, yet [] == false is true, because == follows different coercion rules.
    What interviewers listen for
    • Falsy: false, 0, -0, 0n, '', null, undefined, NaN
    • Everything else is truthy, including [], {} and '0'
    • || replaces any falsy value; ?? only nullish ones
    • Convert explicitly with !! or Boolean()

    Likely follow-up: Why is [] == false true when [] is truthy?

  25. 25.What is a callback, and what is "callback hell"? How do you avoid it?easy

    A callback is a function passed to another function to be called later: when an event fires, a timer finishes or an async operation completes. Callbacks can be synchronous (array.map(fn)) or asynchronous (setTimeout(fn), addEventListener('click', fn)). Node's classic convention is the error-first callback, (err, result) => {}.

    Callback hell, or the "pyramid of doom", is what you get when async steps depend on each other and each one is nested inside the previous callback. The problems:

    • The code drifts to the right and becomes hard to read and change.
    • Error handling is repeated at every level, and exceptions thrown inside async callbacks can't be caught by an outer try/catch.
    • Inversion of control: you trust the other code to call your callback exactly once, with the right arguments.

    The fixes: extract named functions to flatten the nesting, move to promises (a flat chain with one catch), and ideally async/await, which reads top to bottom with normal try/catch. In Node, util.promisify converts error-first APIs.

    getUser(id, (err, user) => {
      if (err) return handle(err);
      getOrders(user, (err, orders) => {
        if (err) return handle(err);
        getInvoice(orders[0], (err, invoice) => { /* ...and deeper */ });
      });
    });
    
    // The same flow with promise-based APIs and async/await
    const user = await getUser(id);
    const orders = await getOrders(user);
    const invoice = await getInvoice(orders[0]);
    What interviewers listen for
    • A function passed in to be called later
    • Error-first convention: (err, result)
    • Nesting dependent async steps causes callback hell
    • Hurts readability, error handling; inverts control
    • Fix with named functions, promises, async/await

    Likely follow-up: How would you convert a callback-based function into one that returns a promise?

  26. 26.What is a higher-order function? Give some examples.easy

    A higher-order function is a function that takes another function as an argument, returns a function, or both. That works because functions in JavaScript are first-class values: you can store them in variables, pass them around and return them like any other value.

    Examples:

    • Taking functions: map, filter, reduce, sort with a comparator, setTimeout, addEventListener. They separate what to do with each item from the mechanics of looping or scheduling.
    • Returning functions: factories such as multiplier(3), and utilities like debounce, throttle, memoize, once and curry, which wrap a function and return an enhanced version.
    • Both: function composition, where compose(f, g) returns x => f(g(x)), and wrappers or decorators such as withLogging(fn).

    The benefits are reusable, composable, declarative code with less duplication. The main cost is readability when the abstractions get too clever.

    What interviewers listen for
    • Takes a function, returns a function, or both
    • Possible because functions are first-class values
    • Built-ins: map, filter, reduce, setTimeout
    • Utilities: debounce, memoize, once, compose
    • Enables reusable, composable abstractions
  27. 27.What is currying? How would you implement a generic curry function?mid

    Currying transforms a function that takes several arguments, f(a, b, c), into a chain of functions that each take one argument, f(a)(b)(c). It's related to, but not the same as, partial application, which fixes some arguments and returns a function that expects the rest; bind does partial application.

    Why it's useful: you can derive specialized functions from general ones, e.g. const logError = curriedLog('error'), and curried functions compose nicely with map and other higher-order functions.

    A generic implementation relies on fn.length, the number of declared parameters. The curried function collects arguments across calls; once it has at least fn.length of them it calls the original, otherwise it returns a function waiting for more. This version also accepts several arguments per call, like cAdd(1, 2)(3), as most library versions do.

    Caveat: fn.length doesn't count a rest parameter or any parameter from the first default value onward, so variadic functions need an explicit arity.

    function curry(fn) {
      return function curried(...args) {
        if (args.length >= fn.length) return fn.apply(this, args);
        return (...more) => curried.apply(this, [...args, ...more]);
      };
    }
    
    const add = (a, b, c) => a + b + c;
    const cAdd = curry(add);
    cAdd(1)(2)(3); // 6
    cAdd(1, 2)(3); // 6
    What interviewers listen for
    • Turns f(a, b, c) into f(a)(b)(c)
    • Related to, but different from, partial application
    • Generic curry collects args until fn.length is reached
    • Useful for specialized, composable functions
    • fn.length excludes rest and defaulted parameters

    Likely follow-up: How would you support an open-ended curry like sum(1)(2)(3)()?

  28. 28.What is memoization? Implement a simple memoize function.mid

    Memoization caches a function's results by its inputs, so repeated calls with the same arguments return the stored result instead of recomputing it. It trades memory for speed.

    A basic memoize is a higher-order function that closes over a cache (a Map), builds a key from the arguments, and returns the cached value if there is one; otherwise it computes, stores and returns the result.

    Points interviewers want to hear:

    • It's only correct for pure functions: same input, same output, no side effects. Memoizing something that depends on time or external state returns stale results.
    • Cache keys need thought: JSON.stringify(args) is simple but slow for big inputs and can't tell apart objects with equal contents; a single primitive argument can be the key directly; a WeakMap suits object arguments.
    • Memory growth: an unbounded cache can act like a leak, so real implementations cap the size (LRU) or expire entries.
    • Recursive functions like Fibonacci benefit hugely when the recursive calls go through the memoized version: exponential time becomes linear.
    function memoize(fn) {
      const cache = new Map();
      return function (...args) {
        const key = JSON.stringify(args);
        if (!cache.has(key)) cache.set(key, fn.apply(this, args));
        return cache.get(key);
      };
    }
    
    const fib = memoize((n) => (n < 2 ? n : fib(n - 1) + fib(n - 2)));
    fib(50); // 12586269025, returned almost instantly
    What interviewers listen for
    • Cache results keyed by the arguments
    • Only safe for pure functions
    • Choose keys carefully: primitives, JSON.stringify, WeakMap
    • Unbounded caches grow; consider an LRU limit
    • Memoized recursion turns exponential time into linear

    Likely follow-up: How would you add a maximum cache size?

  29. 29.Compare localStorage, sessionStorage and cookies. When would you use each?mid

    All three store small amounts of data in the browser, but they behave differently:

    • localStorage is scoped to the origin, persists until code or the user clears it, and is shared by all tabs of that origin. Other tabs receive a storage event when it changes. Good for preferences like the theme.
    • sessionStorage is scoped to the origin and the tab: it survives reloads but is cleared when the tab closes, and each tab has its own copy (duplicating a tab copies it). Good for per-tab state such as a multi-step form draft.
    • Cookies are sent to the server with every matching HTTP request, which is their real purpose: sessions and authentication. They're limited to about 4 KB each, can expire, and support attributes like HttpOnly (hidden from JavaScript), Secure (HTTPS only) and SameSite (CSRF protection).

    Web Storage only holds strings (use JSON.stringify), is synchronous, and typically allows around 5 MB per origin. Anything JavaScript can read, an XSS attack can steal, so session tokens are safer in HttpOnly cookies. For large or structured data, use IndexedDB.

    What interviewers listen for
    • localStorage: persistent, shared across tabs of an origin
    • sessionStorage: per tab, cleared when the tab closes
    • Cookies travel with every request; about 4 KB each
    • HttpOnly, Secure and SameSite protect cookies
    • Web Storage is string-only, synchronous and readable by XSS

    Likely follow-up: Where would you store an authentication token, and why?

  30. 30.How do ES6 classes work, and how are they different from constructor functions?mid

    A class is largely syntax over prototypes: methods in the class body are placed on ClassName.prototype, extends links the prototype chains, and typeof MyClass is "function". But classes aren't only sugar; they differ from constructor functions in real ways:

    • They must be called with new; calling one as a plain function throws a TypeError.
    • The class body always runs in strict mode.
    • Methods are non-enumerable, so they don't appear in for...in loops.
    • Class declarations stay in the TDZ until evaluated, so you can't use a class before its declaration.
    • In a derived class, you must call super() before touching this in the constructor, or you get a ReferenceError.

    Classes also bring features without a clean older equivalent: truly private fields and methods (#sound), static members and static initialization blocks, and public class fields. Inside a method, super.method() calls the parent's version of an overridden method.

    class Animal {
      #sound; // private field
      static kind = 'animal'; // static member: Animal.kind
      constructor(name, sound) {
        this.name = name;
        this.#sound = sound;
      }
      speak() { return `${this.name} says ${this.#sound}`; }
    }
    class Dog extends Animal {
      constructor(name) { super(name, 'woof'); } // must call super() first
    }
    What interviewers listen for
    • Methods live on the prototype; extends links prototypes
    • Must be called with new; the body is strict
    • Methods are non-enumerable; declarations are in the TDZ
    • Call super() before using this in derived constructors
    • Private #fields, static members, class fields

    Likely follow-up: How would you get private state without # fields?

  31. 31.What do optional chaining (?.) and nullish coalescing (??) do?easy

    Optional chaining (?.) safely accesses something that might be null or undefined. If the value to its left is nullish, the whole expression short-circuits to undefined instead of throwing a TypeError. Its three forms are obj?.prop, obj?.[key] and fn?.(), which calls a function only if it exists.

    Nullish coalescing (??) returns its right-hand side only when the left side is null or undefined. That's the key difference from ||, which falls back on any falsy value, so 0, '' and false get replaced. settings.volume ?? 50 keeps a volume of 0; settings.volume || 50 turns it into 50.

    Details worth knowing:

    • Short-circuiting skips the rest of the chain, including calls in it.
    • ?. only guards the value on its left: if a exists but a.b is missing, a?.b.c still throws.
    • You can't assign through it: obj?.x = 1 is a SyntaxError.
    • Mixing ?? with || or && requires parentheses.
    • ??= assigns only when the target is nullish.

    Don't overuse it: if a value must exist, a loud error beats a silent undefined.

    What interviewers listen for
    • ?. short-circuits to undefined on null or undefined
    • Forms: a?.b, a?.[key], fn?.()
    • ?? falls back only for null or undefined
    • || also replaces 0, '' and false
    • No assignment through ?.; parenthesize ?? with ||
  32. 32.What happens, step by step, when you call a function with new? Could you implement it yourself?hard

    new Foo(...args) does four things:

    • Creates a new, empty object.
    • Links its prototype: the object's [[Prototype]] is set to Foo.prototype, which is how instances share methods.
    • Calls Foo with this bound to the new object and the given arguments, so the constructor can add properties.
    • Returns the new object, unless the constructor explicitly returns an object (including an array or function), in which case that object is returned instead. Returning a primitive is ignored.

    You can emulate it with Object.create and apply, as in the snippet. The real operator does a bit more: inside the constructor, new.target refers to the function that was invoked with new, and for derived classes the object is created by the base constructor, which is why they must call super() before using this. That also means myNew can't construct classes, since class constructors throw when called without new.

    Not every function is a constructor: arrow functions and shorthand object methods throw a TypeError when used with new.

    function myNew(Ctor, ...args) {
      const obj = Object.create(Ctor.prototype); // create + link prototype
      const result = Ctor.apply(obj, args);      // run constructor with this = obj
      const isObject = result !== null &&
        (typeof result === 'object' || typeof result === 'function');
      return isObject ? result : obj;            // an explicit object return wins
    }
    
    function Person(name) { this.name = name; }
    Person.prototype.hi = function () { return `Hi, ${this.name}`; };
    myNew(Person, 'Ada').hi(); // "Hi, Ada"
    What interviewers listen for
    • Creates an empty object
    • Sets its prototype to Constructor.prototype
    • Calls the constructor with this set to the new object
    • Returns it, unless the constructor returns an object
    • Arrow functions and methods can't be used with new

    Likely follow-up: What is new.target used for?

  33. 33.What are the differences between ES modules and CommonJS?hard

    CommonJS (require and module.exports) is Node's original module system. ES modules (import and export) are the language standard, used by browsers, bundlers and modern Node.

    • Static versus dynamic: import declarations are static and hoisted, so the dependency graph is known before any code runs. That enables tree-shaking and early errors for missing named exports. require is an ordinary function call evaluated at runtime, so it can be conditional or computed. ESM offers import() for dynamic, promise-based loading.
    • Loading: require loads and executes a module synchronously. ESM is designed for asynchronous loading and supports top-level await.
    • Bindings: ES imports are live, read-only bindings; when the exporting module updates an exported let, importers see the new value. With CommonJS you get whatever module.exports holds, so an exported primitive is a copy.
    • Defaults: ESM is always in strict mode, top-level this is undefined, and there's no require or __dirname; use import.meta.url instead.

    In Node, the .mjs extension or "type": "module" in package.json selects ESM. ESM can import CommonJS modules directly; the reverse has traditionally required a dynamic import().

    // counter.mjs
    export let count = 0;
    export function increment() { count++; }
    
    // main.mjs
    import { count, increment } from './counter.mjs';
    increment();
    console.log(count); // 1: live binding
    
    // A CommonJS module doing `module.exports = { count, increment }`
    // exports a copy of `count`, so the importer would still see 0.
    What interviewers listen for
    • ESM is static and hoisted; require runs at runtime
    • Static imports enable tree-shaking and early errors
    • ESM imports are live, read-only bindings
    • ESM: strict mode, top-level await, no __dirname
    • import() loads ES modules dynamically

    Likely follow-up: Why does tree-shaking work better with ES modules?

  34. 34.How do you handle errors in asynchronous code? What are the common pitfalls?hard

    With promise chains you handle errors with .catch(); with async/await you use ordinary try/catch, because await rethrows the rejection reason. finally runs cleanup either way without swallowing the error.

    Common pitfalls:

    • try/catch can't catch errors thrown in callbacks that run later, such as inside a setTimeout, because the try block has already finished. Handle the error inside the callback, or promisify it.
    • Missing await: return fetchData() inside a try hands back the promise without waiting, so its rejection bypasses your catch. Use return await there.
    • Unhandled rejections: a rejected promise with no handler fires unhandledrejection in browsers and, by default, crashes the process in Node. Always await or attach a catch, even for fire-and-forget calls.
    • Swallowing errors: a catch that only logs turns failures into silent undefined values. Rethrow, or wrap with new Error('context', { cause: err }).
    • Fail-fast Promise.all: use allSettled when you need every outcome.

    Also, fetch only rejects on network failures, so check response.ok for HTTP errors.

    async function loadUser() {
      try {
        const res = await fetch('/api/user');
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return await res.json(); // `return await` keeps rejections inside this try
      } catch (err) {
        throw new Error('Failed to load user', { cause: err });
      } finally {
        hideSpinner();
      }
    }
    What interviewers listen for
    • try/catch with await; .catch() on promise chains
    • try can't catch errors from callbacks that run later
    • Use return await inside try blocks
    • Handle every rejection; Node crashes on unhandled ones by default
    • fetch doesn't reject on HTTP error statuses

    Likely follow-up: How would you implement a retry with exponential backoff?

  35. 35.What is the difference between event.preventDefault() and event.stopPropagation()?easy

    They control two independent things:

    • preventDefault() cancels the browser's default action for the event: following a link, submitting a form and reloading the page, toggling a checkbox, opening the context menu. The event still propagates, so ancestor listeners still run. It only works for cancelable events, and it's ignored inside listeners registered with { passive: true }.
    • stopPropagation() stops the event from travelling any further along the capture and bubble path, so listeners on other elements (usually ancestors) never see it. The default action still happens, and other listeners on the same element still run.
    • stopImmediatePropagation() additionally skips the remaining listeners on the same element.

    A common confusion: returning false from an addEventListener callback does nothing. It does prevent the default in an onclick handler property, and in jQuery it means both methods.

    Use stopPropagation sparingly, because it breaks event delegation and "click outside to close" logic higher up the tree. Check event.defaultPrevented to see whether the default was already cancelled.

    What interviewers listen for
    • preventDefault cancels the browser's default action
    • stopPropagation stops the event reaching other elements
    • Neither one implies the other
    • stopImmediatePropagation also skips same-element listeners
    • return false does nothing in addEventListener
  36. 36.What does typeof return for different values, and what are its quirks?easy

    typeof returns one of eight strings: "undefined", "boolean", "number", "bigint", "string", "symbol", "function" or "object". The quirks:

    • typeof null is "object", a bug from the first version of JavaScript that can't be fixed without breaking the web. Check with value === null instead.
    • Arrays, dates, regexes, maps and plain objects are all "object". Use Array.isArray(value) for arrays, and instanceof or Object.prototype.toString.call(value) for finer detail.
    • Functions and classes both report "function", even though functions are objects.
    • typeof NaN is "number".
    • Wrapper objects: typeof new String('a') is "object", while typeof 'a' is "string".
    • typeof undeclaredVariable returns "undefined" instead of throwing, which made it popular for feature detection. However, a let or const variable in its temporal dead zone does throw a ReferenceError.

    A common check for "a real object, not null or an array" is value !== null && typeof value === 'object' && !Array.isArray(value).

    What interviewers listen for
    • Eight possible results, including "bigint" and "symbol"
    • typeof null is "object": check === null
    • Arrays are "object": use Array.isArray
    • Functions and classes both report "function"
    • Safe for undeclared names, but throws in the TDZ
  37. 37.When would you use a Map instead of a plain object?mid

    Both store key-value pairs, but a Map is built to be a dictionary, while objects are built to be records.

    Reasons to choose a Map:

    • Any key type: objects, functions, numbers, even NaN. Object keys are always strings or symbols, so obj[someObject] silently becomes the key "[object Object]".
    • Predictable order: a Map iterates in insertion order. Objects mostly do too, but integer-like keys are listed first, in ascending order.
    • No inherited keys: plain objects inherit from Object.prototype, so keys like constructor or __proto__ can collide. Object.create(null) avoids that; a Map never has the problem.
    • A real API: size, has, delete, clear, and direct iteration with for...of.
    • Frequent additions and removals: engines optimize Map for this, while objects are optimized for a stable shape.

    Reasons to keep an object: a fixed set of known fields, JSON serialization (JSON.stringify(map) gives "{}"), destructuring, and literal syntax.

    What interviewers listen for
    • Map keys can be any type; object keys are strings or symbols
    • Map keeps insertion order for every key
    • No prototype key collisions in a Map
    • size, has, direct iteration, cheap add and delete
    • Objects are better for fixed records and JSON

    Likely follow-up: When would you use a WeakMap instead?

  38. 38.What is a Set, and when would you use one?easy

    A Set is a collection of unique values of any type; adding a value that's already there does nothing. Uniqueness uses the SameValueZero comparison, which is like === except that NaN equals NaN. Objects are compared by reference, so two different objects with identical contents are both kept.

    The API: add, has, delete, clear, size, and iteration in insertion order with for...of, forEach or spread.

    Typical uses:

    • Removing duplicates: [...new Set(array)].
    • Fast membership checks: set.has(x) takes roughly constant time on average, while array.includes(x) scans the whole array. That matters for large collections checked repeatedly, such as visited nodes in a graph traversal or selected ids in a UI.
    • Set operations: newer engines include union, intersection, difference and isSubsetOf; otherwise, combine spread with filter and has.

    A Set has no index access and doesn't serialize to JSON directly; convert it with Array.from(set) first. For objects that the collection shouldn't keep alive, use a WeakSet.

    What interviewers listen for
    • Stores unique values of any type
    • SameValueZero: NaN equals NaN; objects by reference
    • Deduplicate with [...new Set(arr)]
    • has is fast compared with array.includes
    • Insertion-ordered iteration; no index access
  39. 39.Implement Promise.all from scratch.hard

    Promise.all returns a new promise that fulfills with an array of results, in input order, once every input fulfills, and rejects as soon as any input rejects. My implementation:

    • Spread the iterable into an array inside the executor, so a non-iterable argument rejects instead of throwing synchronously.
    • If the input is empty, resolve with [] right away; otherwise the promise would never settle.
    • Wrap each item in Promise.resolve(item), so plain values and thenables work too.
    • Store each value at its own index instead of pushing, because promises settle in any order.
    • Count completions with a remaining counter and resolve when it reaches zero. Checking results.length doesn't work, because the preallocated array already has its full length.
    • Pass reject as the rejection handler, so the first rejection rejects the result. Later settlements are harmless, since a promise can only settle once.

    Note that the inputs are already running when promiseAll is called; it only waits for them, which is why they run in parallel, and it doesn't cancel anything on failure.

    function promiseAll(iterable) {
      return new Promise((resolve, reject) => {
        const items = [...iterable]; // throwing here rejects the promise
        const results = new Array(items.length);
        let remaining = items.length;
        if (remaining === 0) resolve(results);
        items.forEach((item, i) => Promise.resolve(item).then((value) => {
          results[i] = value;
          if (--remaining === 0) resolve(results);
        }, reject));
      });
    }
    What interviewers listen for
    • Return a new promise; resolve [] for empty input
    • Wrap items in Promise.resolve to accept plain values
    • Store results by index to preserve order
    • Count completions rather than checking the array length
    • The first rejection wins; nothing gets cancelled

    Likely follow-up: How would you change it to implement Promise.allSettled? · How would you limit it to N promises in flight at once?

  40. 40.How does garbage collection work in JavaScript, and what commonly causes memory leaks in front-end apps?hard

    JavaScript engines manage memory with a tracing garbage collector, conceptually mark-and-sweep: starting from the roots (the global object and the variables on the current call stack), the GC marks everything reachable, and anything unreachable gets freed. Because it's based on reachability, circular references between otherwise unreachable objects are still collected. Modern engines are also generational: most objects die young, so new objects are collected often and cheaply.

    A leak is memory that's still reachable but no longer needed. Common causes:

    • Forgotten timers and intervals whose callbacks reference components or data.
    • Event listeners and subscriptions that are never removed, e.g. on window or from an unmounted component.
    • Detached DOM nodes: removed from the page but still referenced from JavaScript.
    • Closures that capture large objects and live for a long time.
    • Unbounded caches and maps that keep temporary objects forever.

    To diagnose, use the DevTools Memory panel: take heap snapshots before and after repeating an action and look for growing object counts. Fixes include cleanup functions, removeEventListener or an AbortController signal, WeakMap, and bounded caches.

    What interviewers listen for
    • Tracing GC frees whatever is unreachable from the roots
    • Circular references are fine for mark-and-sweep
    • A leak is memory still reachable but no longer needed
    • Causes: timers, listeners, detached DOM, closures, caches
    • Diagnose with heap snapshots; fix with cleanup and weak references

    Likely follow-up: How would you find a leak using Chrome DevTools?

  41. 41.What are WeakMap and WeakSet, and when would you use them?hard

    WeakMap and WeakSet are like Map and Set, except that they hold their keys weakly: an entry doesn't stop its key from being garbage collected. Once nothing else references the key object, the entry can disappear automatically.

    That design has consequences:

    • Keys (and WeakSet values) must be objects; primitives throw a TypeError, because they can't be garbage collected. Newer engines also accept non-registered symbols.
    • They're not iterable and have no size or clear, since their contents depend on when the GC runs. The API is just get, set, has and delete (add, has and delete for WeakSet).

    Use cases:

    • Attaching metadata or private data to objects you don't own, such as DOM nodes or library objects, without mutating them and without leaking memory after they're gone.
    • Per-object caches: memoizing an expensive computation keyed by an object.
    • Tracking objects: a WeakSet of processed nodes, or of visited objects to detect cycles in a deep clone.

    Before #private fields existed, a module-level WeakMap was the standard way to store private instance state.

    What interviewers listen for
    • Keys are held weakly and don't block garbage collection
    • Keys must be objects, not primitives
    • Not iterable; no size or clear
    • Use for per-object metadata, caches and private data
    • Avoids leaks when the key object goes away

    Likely follow-up: Why can't you iterate over a WeakMap?

  42. 42.What is strict mode, and what does it change?mid

    Strict mode is an opt-in, more restrictive variant of JavaScript that turns silent mistakes into errors and removes some confusing features. You enable it with 'use strict'; at the top of a script or a function. ES modules and class bodies are strict automatically, so most modern code already runs in strict mode.

    The main changes:

    • Assigning to an undeclared variable throws a ReferenceError instead of creating a global.
    • this is undefined in plain function calls instead of the global object.
    • Writing to a read-only property, adding a property to a frozen object, or deleting a non-configurable property throws a TypeError instead of failing silently.
    • Duplicate parameter names, the with statement and legacy octal literals like 010 are syntax errors.
    • eval can't create variables in the surrounding scope, and arguments no longer stays in sync with the named parameters.
    • Reserved words such as let, static, implements and private can't be used as identifiers.

    One gotcha: a function with default, rest or destructured parameters can't contain its own 'use strict' directive.

    What interviewers listen for
    • Opt in with 'use strict'; modules and classes are strict by default
    • Assigning an undeclared variable throws ReferenceError
    • this is undefined in plain function calls
    • Silent failures become TypeErrors
    • Bans with, duplicate parameters and octal literals
  43. 43.What is an IIFE, and why was it used? Is it still useful today?easy

    An IIFE (Immediately Invoked Function Expression) is a function that's defined and called in one step: (function () { ... })(). The wrapping parentheses make the parser treat it as an expression rather than a function declaration, which couldn't be invoked directly.

    Why it became popular:

    • Private scope: before let, const and modules, the only scope boundary was a function. Wrapping code in an IIFE kept its variables out of the global scope and avoided collisions between scripts.
    • The module pattern: an IIFE could return an object exposing a public API while everything else stayed private in its closure. Libraries like jQuery shipped this way.
    • Capturing loop values with var, by passing the current value in as an argument.

    Today ES modules give every file its own scope, and block scoping fixes the loop problem, so IIFEs are much rarer. They're still handy as an async IIFE where top-level await isn't available, for one-off setup code with its own local variables, and in bundler output.

    What interviewers listen for
    • A function expression invoked immediately
    • Creates a private scope and avoids global pollution
    • The basis of the classic module pattern
    • Largely replaced by ES modules and block scope
    • Still useful as an async IIFE for await
  44. 44.How do you make an object immutable in JavaScript? How does Object.freeze differ from const?mid

    const and Object.freeze protect different things. const makes the binding immutable: the variable can't be reassigned, but the object it points to can still change. Object.freeze(obj) makes the object immutable: you can't add, remove or change its properties. In strict mode, such attempts throw a TypeError; in sloppy mode, they fail silently.

    Important details:

    • Object.freeze is shallow: nested objects and arrays inside a frozen object stay mutable. Full immutability needs a recursive deep freeze.
    • Weaker options: Object.seal prevents adding and deleting properties but allows changing existing ones; Object.preventExtensions only prevents adding.
    • Check with Object.isFrozen, Object.isSealed and Object.isExtensible.

    Day to day, immutability is usually a convention rather than enforcement: create new values instead of mutating, using spread, map, filter, and non-mutating array methods like toSorted, toReversed, toSpliced and with. That makes change detection by reference (prev !== next) reliable, which React, Redux and Angular's OnPush rely on. Libraries like Immer let you write "mutating" code that produces immutable updates.

    What interviewers listen for
    • const locks the binding, not the object
    • Object.freeze blocks adding, removing and changing properties
    • Freeze is shallow; deep freeze needs recursion
    • Throws in strict mode, fails silently otherwise
    • Prefer non-mutating updates: spread, map, toSorted

    Likely follow-up: What is the difference between Object.freeze and Object.seal?

  45. 45.What is a pure function, and what are side effects?easy

    A pure function has two properties:

    • Deterministic: given the same arguments, it always returns the same result.
    • No side effects: it doesn't change anything outside itself, and doesn't depend on anything that can change.

    A side effect is any observable interaction with the outside world: mutating an argument or a global variable, updating the DOM, making a network request, logging, reading localStorage, or calling Date.now() or Math.random().

    Why pure functions matter:

    • They're easy to test: no mocks, just inputs and expected outputs.
    • They're predictable, and safe to reuse, reorder, memoize or run in parallel.
    • Frameworks depend on purity: React expects components to render purely, and Redux reducers must return new state instead of mutating it.

    A common accidental impurity is mutating an argument: arr.sort() sorts in place, so calling it on an input changes the caller's array; toSorted() or [...arr].sort() avoids that.

    Real apps need side effects, of course. The goal is to keep core logic pure and push effects to the edges: event handlers, effects and services.

    What interviewers listen for
    • Same input always produces the same output
    • No side effects: no mutation, I/O or hidden state
    • Easy to test, memoize and reason about
    • React rendering and Redux reducers must be pure
    • Keep logic pure; push side effects to the edges
  46. 46.Why does 0.1 + 0.2 === 0.3 evaluate to false, and how do you deal with it?easy

    JavaScript numbers are IEEE 754 double-precision binary floating-point values. Just as 1/3 can't be written exactly in decimal, most decimal fractions, including 0.1 and 0.2, can't be represented exactly in binary. Each is stored as the nearest representable value, the tiny errors combine, and 0.1 + 0.2 gives 0.30000000000000004. It isn't a JavaScript bug: every language that uses doubles behaves the same way.

    How to deal with it:

    • Comparisons: check that the difference is within a tolerance instead of using ===, e.g. Math.abs(a - b) < Number.EPSILON for values around 1, or a relative tolerance for larger numbers.
    • Display: round for output with toFixed(2) or Intl.NumberFormat. Note that toFixed returns a string and has its own surprises: (1.005).toFixed(2) is "1.00", because 1.005 is actually stored slightly below that value.
    • Money: store integer cents, or use a decimal library, instead of fractional amounts.
    • Large integers: doubles are exact only up to Number.MAX_SAFE_INTEGER (2 ** 53 - 1); beyond that, use BigInt.
    What interviewers listen for
    • Numbers are IEEE 754 binary doubles
    • 0.1 and 0.2 have no exact binary representation
    • Compare with a tolerance such as Number.EPSILON
    • Use integer cents or a decimal library for money
    • Integers are exact only up to Number.MAX_SAFE_INTEGER
  47. 47.What are reflow and repaint? How do you avoid performance problems when manipulating the DOM?hard

    When the DOM or styles change, the browser runs a rendering pipeline: style calculation, layout (also called reflow: computing each element's size and position), paint (drawing the pixels) and compositing the layers together.

    • A reflow is caused by geometry changes: adding or removing elements, or changing width, height, margins, fonts or content. It's the most expensive step and can affect descendants, siblings and ancestors.
    • A repaint happens for visual changes that don't affect layout, such as color, background or visibility.
    • Changes to transform and opacity can often be handled by the compositor alone, skipping layout and paint, which is why animations should use them.

    The classic trap is layout thrashing: reading a layout property like offsetHeight, getBoundingClientRect() or scrollTop right after a write forces the browser to recalculate layout synchronously, and doing that in a loop is very slow.

    Best practices: batch all reads before all writes, toggle a CSS class instead of setting many inline styles, build nodes in a DocumentFragment and insert them once, schedule visual updates with requestAnimationFrame, and virtualize very long lists.

    const boxes = [...document.querySelectorAll('.box')];
    
    // Layout thrashing: each read forces layout after the previous write
    for (const box of boxes) {
      box.style.width = `${box.offsetWidth + 10}px`;
    }
    
    // Better: read everything first, then write everything
    const widths = boxes.map((box) => box.offsetWidth);
    boxes.forEach((box, i) => {
      box.style.width = `${widths[i] + 10}px`;
    });
    What interviewers listen for
    • Pipeline: style, layout (reflow), paint, composite
    • Reflow recalculates geometry and is the most expensive step
    • Repaint redraws pixels without changing layout
    • Avoid layout thrashing: batch reads, then writes
    • Animate transform/opacity; use fragments and requestAnimationFrame

    Likely follow-up: Which CSS properties can be animated without triggering layout?

  48. 48.What are iterators and generators in JavaScript? Give a use case for each.hard

    Under the iterator protocol, an iterator is an object with a next() method that returns { value, done }. An iterable is an object with a [Symbol.iterator]() method that returns an iterator. Arrays, strings, Map, Set and NodeList are iterable, and that's what for...of, spread, destructuring and Array.from consume. Plain objects aren't iterable.

    A generator function (function*) is the easy way to create iterators. Calling it doesn't run the body; it returns a generator object that is both an iterator and an iterable. Each next() runs the body up to the next yield, pauses there, and returns the yielded value. A return value arrives with done: true, and for...of and spread ignore it.

    Use cases:

    • Lazy or infinite sequences: ids, ranges and pagination, computed only on demand.
    • Custom iterables: a class with a *[Symbol.iterator]() method works with for...of.
    • Two-way communication: next(value) sends a value back in as the result of yield, the idea behind redux-saga.
    • Async generators (async function*) with for await...of for streaming data, such as paginated APIs.
    function* idGenerator() {
      let id = 1;
      while (true) yield id++; // infinite, but lazy
    }
    const ids = idGenerator();
    ids.next(); // { value: 1, done: false }
    
    class Range {
      constructor(from, to) { this.from = from; this.to = to; }
      *[Symbol.iterator]() { for (let i = this.from; i <= this.to; i++) yield i; }
    }
    [...new Range(1, 3)]; // [1, 2, 3]
    What interviewers listen for
    • Iterator: next() returns { value, done }
    • Iterable: has a [Symbol.iterator]() method
    • function* returns a pausable generator; the body runs lazily
    • for...of, spread and destructuring consume iterables
    • Uses: lazy sequences, custom iterables, async streams

    Likely follow-up: How would you make a plain object iterable?

  49. 49.What is a Symbol, and what is it used for?hard

    A Symbol is a primitive whose values are guaranteed to be unique: Symbol('id') === Symbol('id') is false, and the string is only a description for debugging. You create one with Symbol() (new Symbol() throws), or use Symbol.for(key) to get a shared symbol from a global registry.

    Main uses:

    • Collision-free property keys: libraries can attach metadata to objects without clashing with anyone's string keys.
    • Hidden-ish properties: symbol keys are skipped by for...in, Object.keys and JSON.stringify. They are not private: Object.getOwnPropertySymbols and Reflect.ownKeys reveal them, and spread and Object.assign copy them. Use #private fields for real privacy.
    • Well-known symbols that customize language behavior: Symbol.iterator makes an object iterable, Symbol.asyncIterator enables for await, Symbol.toPrimitive controls type conversion, Symbol.toStringTag changes what Object.prototype.toString reports, and Symbol.hasInstance customizes instanceof.

    A gotcha: symbols don't convert to strings implicitly, so '' + sym or a template literal throws a TypeError. Use String(sym) or sym.description.

    What interviewers listen for
    • A primitive with guaranteed-unique values
    • Collision-free keys, skipped by Object.keys and JSON
    • Not private: getOwnPropertySymbols reveals them
    • Well-known symbols like Symbol.iterator customize behavior
    • Symbol.for uses a global registry; no implicit string conversion
  50. 50.How do getters, setters and Object.defineProperty work? What are property descriptors?hard

    Getters and setters are accessor properties: reading obj.prop calls a get function, and assigning to it calls a set function. You define them with get/set syntax in object literals and classes, and use them for computed values, validation, lazy initialization, or keeping a public API stable while the internals change.

    Every property has a descriptor, which you can inspect with Object.getOwnPropertyDescriptor:

    • Data properties have value and writable.
    • Accessor properties have get and/or set, and can't also have value or writable.
    • Both kinds have enumerable (visible to Object.keys, for...in and JSON) and configurable (can be deleted or redefined).

    Object.defineProperty(obj, key, descriptor) gives precise control. The catch: attributes you omit default to false, whereas normal assignment creates a property that is writable, enumerable and configurable. So a property defined with only a value is read-only, hidden from Object.keys, and can't be deleted. Writing to it fails silently in sloppy mode and throws in strict mode.

    Vue 2's reactivity was built on Object.defineProperty; Vue 3 moved to Proxy, which can also detect added and deleted properties.

    const temp = {
      celsius: 0,
      get fahrenheit() { return this.celsius * 9 / 5 + 32; },
      set fahrenheit(f) { this.celsius = (f - 32) * 5 / 9; },
    };
    temp.fahrenheit = 212;
    temp.celsius; // 100
    
    const obj = {};
    Object.defineProperty(obj, 'id', { value: 1 }); // omitted flags default to false
    Object.getOwnPropertyDescriptor(obj, 'id');
    // { value: 1, writable: false, enumerable: false, configurable: false }
    What interviewers listen for
    • Getters and setters run code on property reads and writes
    • Descriptors: data (value, writable) or accessor (get, set)
    • enumerable and configurable apply to both kinds
    • defineProperty defaults omitted flags to false
    • Used for validation, computed values and reactivity
  51. 51.What are the common pitfalls of JSON.stringify and JSON.parse?mid

    JSON only supports strings, numbers, booleans, null, arrays and plain objects, so a round trip through JSON.stringify and JSON.parse loses or changes a lot:

    • Silently dropped: object properties whose value is undefined, a function or a symbol, plus all symbol keys. Inside arrays those values become null instead.
    • Converted: NaN and Infinity become null; a Date becomes an ISO string via its toJSON method and does not come back as a Date; Map and Set become {}.
    • Throws: circular references and BigInt values throw a TypeError.
    • Identity lost: class instances come back as plain objects without their prototype, and two references to one object become two separate copies.
    • Precision: integers beyond Number.MAX_SAFE_INTEGER, such as 64-bit ids, lose precision when parsed; send them as strings.
    • Parsing throws a SyntaxError on invalid input, including single quotes, trailing commas and the empty string, so wrap untrusted input in try/catch.

    Useful tools: a replacer or a toJSON method to customize output, and a reviver to restore types like dates while parsing. For deep cloning, prefer structuredClone.

    const data = { when: new Date(0), skip: undefined, fn() {}, n: NaN, list: [undefined] };
    const json = JSON.stringify(data);
    // '{"when":"1970-01-01T00:00:00.000Z","n":null,"list":[null]}'
    
    const parsed = JSON.parse(json, (key, value) =>
      key === 'when' ? new Date(value) : value, // reviver restores the Date
    );
    parsed.when instanceof Date; // true
    
    JSON.parse("{'a': 1}"); // SyntaxError: single quotes aren't valid JSON
    What interviewers listen for
    • undefined, functions and symbols are dropped (or null in arrays)
    • Dates become strings; Map/Set become {}; NaN becomes null
    • Circular references and BigInt throw
    • JSON.parse throws SyntaxError; wrap untrusted input
    • Use a replacer, toJSON or a reviver to customize
  52. 52.What is CORS, and why do you get CORS errors?mid

    Browsers enforce the same-origin policy: scripts can only read responses from the same origin, meaning the same scheme, host and port. A page on https://app.example.com calling https://api.example.com is making a cross-origin request. CORS (Cross-Origin Resource Sharing) lets the server opt in to being read cross-origin, using HTTP headers.

    How it works:

    • The browser sends the request with an Origin header. If the response contains a matching Access-Control-Allow-Origin (that exact origin, or *), the script can read it; otherwise the browser blocks access and logs a CORS error.
    • Requests that aren't "simple", for example PUT or DELETE, a JSON Content-Type, or custom headers like Authorization, first trigger a preflight OPTIONS request, and the server must answer with suitable Access-Control-Allow-Methods and Access-Control-Allow-Headers.
    • To include cookies, the client sets credentials: 'include', and the server must send Access-Control-Allow-Credentials: true with a specific origin rather than *.

    The key insight: CORS is enforced by the browser and fixed on the server, or with a same-origin proxy during development. Tools like curl ignore it, so CORS doesn't protect an API from non-browser clients.

    What interviewers listen for
    • Same-origin policy: scheme, host and port must match
    • The server opts in with Access-Control-Allow-Origin
    • Non-simple requests trigger an OPTIONS preflight
    • Credentials need Allow-Credentials and a specific origin
    • Enforced by browsers; fixed on the server

    Likely follow-up: Which requests count as "simple" and skip the preflight?

  53. 53.What is XSS (cross-site scripting), and how do you prevent it in a front-end app?mid

    Cross-site scripting is an attack where untrusted input ends up running as code in another user's page. The script runs with the site's privileges: it can read the DOM, non-HttpOnly cookies and localStorage tokens, and act as the user.

    The three main kinds:

    • Stored: malicious content saved on the server, such as a comment or profile name, is served to other users.
    • Reflected: the payload comes from the request, like a URL parameter echoed into the page.
    • DOM-based: client-side code takes data from location, postMessage or storage and writes it into a dangerous sink like innerHTML.

    Prevention:

    • Treat data as text: use textContent or framework interpolation, which React, Angular and Vue escape by default, instead of innerHTML, document.write or insertAdjacentHTML.
    • Sanitize HTML you must render with a library such as DOMPurify, and review escape hatches like dangerouslySetInnerHTML and v-html.
    • Never turn strings into code: avoid eval and new Function, and validate URLs so javascript: links can't reach an href.
    • Defense in depth: a Content Security Policy and HttpOnly session cookies.
    const name = new URLSearchParams(location.search).get('name');
    
    // Vulnerable: ?name=<img src=x onerror=alert(document.cookie)>
    greeting.innerHTML = `Hello, ${name}`;
    
    // Safe: the browser treats the value as text, not markup
    greeting.textContent = `Hello, ${name}`;
    What interviewers listen for
    • Untrusted input executed as script in the victim's page
    • Stored, reflected and DOM-based variants
    • Use textContent or framework escaping, not innerHTML
    • Sanitize required HTML with a library like DOMPurify
    • Add a Content Security Policy and HttpOnly cookies

    Likely follow-up: How does a Content Security Policy mitigate XSS?

  54. 54.What are the most common pitfalls when working with Date in JavaScript?mid

    A Date is a timestamp (milliseconds since the Unix epoch) that you read either in the local time zone or in UTC, and most bugs come from that split:

    • Months are zero-based: new Date(2024, 0, 15) is January 15th, while days of the month start at 1.
    • Parsing is inconsistent: a date-only ISO string like '2024-03-10' is parsed as UTC, but a date-time string without an offset like '2024-03-10T09:00' is parsed as local time. Non-ISO formats such as '03/10/2024' are implementation-dependent.
    • Off-by-one days: a UTC-midnight date displayed in a time zone behind UTC shows the previous day.
    • Dates are mutable: setDate and friends change the object in place, so copy before modifying.
    • Silent overflow and invalid dates: new Date(2024, 0, 32) rolls over to February 1st, and bad input produces an Invalid Date whose getTime() is NaN.
    • Daylight saving time: adding 24 hours isn't always "same time tomorrow"; use setDate(d.getDate() + 1).

    Store and send UTC ISO strings, convert to local time only for display with Intl.DateTimeFormat, and consider date-fns or Temporal where available.

    new Date(2024, 0, 15);         // January 15, 2024: months start at 0
    new Date(2024, 0, 32);         // February 1, 2024: overflow is silent
    new Date('2024-03-10');        // midnight UTC (date-only ISO string)
    new Date('2024-03-10T09:00');  // 09:00 local time (no offset given)
    new Date('nope').getTime();    // NaN: an Invalid Date
    
    // In New York (behind UTC), UTC midnight is still the previous evening:
    new Date('2024-03-10').getDate(); // 9
    What interviewers listen for
    • Months are zero-based; days of the month are not
    • Date-only strings parse as UTC; date-time strings as local
    • Dates are mutable; copy before modifying
    • Overflow is silent; invalid dates give NaN
    • Store UTC; format for display with Intl.DateTimeFormat
  55. 55.What are Web Workers, and when would you use them?hard

    Web Workers run JavaScript on a background thread, separate from the main thread that handles rendering and user input. Since a long computation on the main thread freezes the UI, moving it into a worker keeps the page responsive.

    How they work:

    • Create one with new Worker(url) (add { type: 'module' } for import) and communicate by message passing: postMessage to send and a message listener to receive.
    • Messages are copied with the structured clone algorithm, so functions can't be sent. Large binary data like an ArrayBuffer can be transferred instead of copied, which is nearly free. SharedArrayBuffer allows real shared memory but requires the page to be cross-origin isolated.
    • Workers have no DOM access and no window, but they can use fetch, timers, WebSockets and IndexedDB.

    Use cases: parsing large files, image processing, compression, encryption and heavy chart calculations. For small tasks, the cost of starting a worker and copying data isn't worth it.

    Don't confuse them with Service Workers, which act as a network proxy for offline support and caching.

    What interviewers listen for
    • Run scripts on a background thread, keeping the UI responsive
    • Communicate via postMessage; data is structured-cloned
    • Transfer ArrayBuffers to avoid copying
    • No DOM access
    • For CPU-heavy work; Service Workers are for networking

    Likely follow-up: How does a Service Worker differ from a Web Worker?

  56. 56.What are Proxy and Reflect, and what can you build with them?hard

    A Proxy wraps a target object and lets you intercept fundamental operations on it through a handler object with traps: get, set, has (the in operator), deleteProperty, ownKeys, apply (function calls), construct (new) and more.

    Reflect is a namespace of static methods that mirror those traps, such as Reflect.get and Reflect.set, and perform the default behavior. Inside a trap, calling the matching Reflect method forwards the operation correctly, including the receiver that getters and inheritance depend on. Reflect methods also return booleans where older Object methods throw, e.g. Reflect.defineProperty.

    What people build with proxies:

    • Reactivity: Vue 3 tracks property reads and triggers updates on writes.
    • Validation, read-only views and default values.
    • Logging, access tracking and negative array indexes.

    Caveats: every operation pays some overhead; a proxy is not === to its target; objects relying on internal slots or private fields, like Map, Date or class #fields, throw when their methods run through a proxy unless you bind them to the target; and a set trap returning false throws a TypeError in strict mode.

    const user = new Proxy({ name: 'Ada', age: 36 }, {
      get(target, key, receiver) {
        return key in target ? Reflect.get(target, key, receiver) : 'n/a';
      },
      set(target, key, value, receiver) {
        if (key === 'age' && !Number.isInteger(value)) throw new TypeError('age must be an integer');
        return Reflect.set(target, key, value, receiver); // default behavior
      },
    });
    user.email;       // "n/a"
    user.age = 37;    // ok
    user.age = 'old'; // TypeError: age must be an integer
    What interviewers listen for
    • Proxy intercepts operations through handler traps
    • Traps include get, set, has, deleteProperty, apply
    • Reflect performs the default behavior inside traps
    • Used for reactivity, validation, defaults and logging
    • Adds overhead; breaks Map, Date and #private without binding
  57. 57.How do setTimeout, setInterval and requestAnimationFrame differ? Which would you use for animations?hard

    All three schedule callbacks, with different guarantees:

    • setTimeout(fn, ms) runs fn once, no sooner than ms milliseconds later. The delay is a minimum: the callback is a macrotask that waits for the call stack and the microtask queue to empty. Browsers also enforce at least about 4 ms once timers are nested several levels deep, and throttle timers in background tabs.
    • setInterval(fn, ms) repeats every ms milliseconds without waiting for your work to finish, so slow callbacks can bunch up, and it runs until you call clearInterval. A recursive setTimeout guarantees a gap between runs, which is often safer for polling.
    • requestAnimationFrame(fn) runs fn once, just before the browser's next repaint, usually in step with the display's refresh rate. It receives a high-resolution timestamp and pauses in hidden tabs.

    For animations, use requestAnimationFrame: timers aren't synchronized with the display, so they drop or double frames and cause jank. Compute movement from the elapsed time rather than assuming a frame rate, and request the next frame from inside the callback. Simple transitions are often better done in CSS.

    What interviewers listen for
    • setTimeout delay is a minimum, not a guarantee
    • setInterval repeats regardless of how long callbacks take
    • Recursive setTimeout guarantees a gap between runs
    • requestAnimationFrame runs before the next repaint; paused when hidden
    • Animate with requestAnimationFrame, based on elapsed time

Coding round

All challenges →
esc