Interview Guide · 24 min read · Updated

Senior Frontend Interview Guide, Part 1: Modern JavaScript (ES6+)

Every frontend interview, whatever the framework on the job description, eventually comes back to JavaScript. React is "just JavaScript," Module Federation is a bundler feature built on JavaScript modules, and most performance and correctness bugs you will be asked to debug are language bugs…

Featured image: Part 1, Modern JavaScript (ES6+)

This article is Part 1 of 18 in The Senior Frontend Engineer Interview Guide, and includes 28 interview questions with model answers.

Every frontend interview, whatever the framework on the job description, eventually comes back to JavaScript. React is “just JavaScript,” Module Federation is a bundler feature built on JavaScript modules, and most performance and correctness bugs you will be asked to debug are language bugs wearing a framework costume. This chapter reviews the parts of the language that interviewers probe most often: scope and closures, the this keyword, prototypes and classes, the event loop, promises and async/await, modules, and the modern syntax that arrived with ES2015 and the yearly releases after it.

Why the Language Still Matters

Senior candidates sometimes assume that language trivia is reserved for junior interviews. In practice, the opposite is true. A senior engineer is expected to explain why a stale closure appears in a useEffect, why a promise chain swallowed an error, or why a for...in loop iterated over an unexpected key. These are questions about the language, and the answers separate people who have memorized APIs from people who understand the runtime.

A good mental model of JavaScript has four layers:

  1. Values and types. Seven primitive types (string, number, bigint, boolean, undefined, symbol, null) and one structural type, object, which includes arrays, functions, dates, maps, and everything else.
  2. Scope. Where a name is visible, decided at author time by the position of the code (lexical scope).
  3. Execution context. What this refers to, decided at call time by how a function is invoked.
  4. The event loop. How the single JavaScript thread interleaves synchronous code, microtasks, and macrotasks.

If you can reason clearly about each layer, you can answer almost any language question from first principles.

Variables: var, let, and const

ES2015 introduced block-scoped declarations. The differences are worth stating precisely.

Feature var let const
Scope Function Block Block
Hoisted Yes, initialized to undefined Yes, but uninitialized (TDZ) Yes, but uninitialized (TDZ)
Re-declaration in same scope Allowed Error Error
Re-assignment Allowed Allowed Error
Creates property on global object Yes (at top level of scripts) No No

The temporal dead zone (TDZ) is the region between the start of a block and the line where a let or const is declared. Accessing the variable in that region throws a ReferenceError, even though the binding technically exists.

console.log(a); // undefined  (var is hoisted and initialized)
console.log(b); // ReferenceError: Cannot access 'b' before initialization
var a = 1;
let b = 2;

const prevents re-assignment of the binding, not mutation of the value. A const array can still be pushed to. To make an object shallowly immutable, use Object.freeze, and remember that freeze is not deep.

The classic loop question demonstrates why block scope matters:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0); // 0, 1, 2
}

With var there is one i shared by all three callbacks, and by the time they run the loop has finished. With let, the specification creates a fresh binding for each iteration, so each callback closes over its own j.

Scope and Closures

A closure is a function together with the lexical environment in which it was created. Every function in JavaScript is a closure; the term usually comes up when a function outlives the scope that created it and still has access to that scope’s variables.

function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    get value() { return count; },
  };
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.value); // 2

count is not reachable from outside, which makes closures the original way to create private state in JavaScript. Closures power module patterns, memoization, partial application, event handlers, and every React hook.

Closures capture variables, not values. If the variable changes, the closure sees the change. This is the root of the “stale closure” bug in React: a callback created during one render captured the variables of that render, and if the callback is reused later (for example inside a setInterval that was set up once), it keeps reading the old values.

function useBrokenTimer() {
  const [seconds, setSeconds] = React.useState(0);
  React.useEffect(() => {
    const id = setInterval(() => setSeconds(seconds + 1), 1000);
    return () => clearInterval(id);
  }, []); // seconds is always 0 inside this closure
  return seconds; // gets stuck at 1
}

The fix is either a functional update (setSeconds(s => s + 1)) or listing seconds as a dependency so the effect re-subscribes.

Memory and Closures

Because a closure keeps its environment alive, it can keep large objects alive too. A long-lived event listener that closes over a big data structure prevents that structure from being garbage collected. Modern engines are smart about only retaining variables the closure actually references, but the rule of thumb holds: remove listeners and clear timers when a component unmounts.

Hoisting

Before code in a scope runs, the engine registers all declarations in that scope. Function declarations are hoisted with their bodies, so you can call them before the line where they appear. var declarations are hoisted and initialized to undefined. let, const, and class declarations are hoisted but stay uninitialized until execution reaches them. Function expressions follow the rules of the variable they are assigned to.

sayHi();            // works
function sayHi() { console.log('hi'); }

sayBye();           // TypeError: sayBye is not a function
var sayBye = function () { console.log('bye'); };

The this Keyword

this is determined by how a function is called, not where it is defined (with the important exception of arrow functions). There are five rules, in order of precedence:

  1. new binding. new Foo() creates a fresh object and binds this to it.
  2. Explicit binding. fn.call(obj), fn.apply(obj), or a function produced by fn.bind(obj) uses obj.
  3. Implicit binding. obj.fn() uses obj.
  4. Default binding. A plain call fn() uses undefined in strict mode (and in modules and classes, which are always strict) or the global object in sloppy mode.
  5. Arrow functions do not have their own this. They use this from the enclosing lexical scope, and call, apply, and bind cannot change it.

The most common bug is losing implicit binding when a method is passed as a callback:

class Button {
  constructor(label) { this.label = label; }
  handleClick() { console.log(this.label); }
}
const b = new Button('Save');
document.body.addEventListener('click', b.handleClick); // this is the element, not b
document.body.addEventListener('click', () => b.handleClick()); // works
document.body.addEventListener('click', b.handleClick.bind(b)); // works

In class components, this is why handlers were bound in the constructor or written as class-field arrow functions. In function components the problem disappears because there is no this.

Prototypes and Classes

Every object has an internal link, [[Prototype]], to another object or null. When you read a property that the object does not have, the engine follows the chain. class syntax is a cleaner way to set up that chain; it does not introduce a new object model.

class Account {
  #balance = 0;                 // private field (ES2022)
  static count = 0;             // static field
  constructor(owner) {
    this.owner = owner;
    Account.count++;
  }
  deposit(amount) {
    if (amount <= 0) throw new RangeError('Amount must be positive');
    this.#balance += amount;
    return this;
  }
  get balance() { return this.#balance; }
}

class SavingsAccount extends Account {
  constructor(owner, rate) {
    super(owner);               // must call before using this
    this.rate = rate;
  }
  addInterest() { return this.deposit(this.balance * this.rate); }
}

Key points interviewers look for:

  • Methods defined in a class body live on Account.prototype, so they are shared by all instances.
  • Class fields (#balance = 0, rate = ...) are created per instance.
  • Private fields (#name) are truly private: they are not accessible through reflection or Object.keys, and accessing them on the wrong object throws.
  • super(...) must be called in a derived constructor before this is touched.
  • Class bodies are always in strict mode, and class declarations are not hoisted in a usable way (they live in the TDZ).

Equality and Type Coercion

=== compares without coercion. == coerces according to a set of rules that are worth knowing only well enough to avoid. The one idiom many teams allow is value == null, which is true for both null and undefined.

Object.is is like === except that Object.is(NaN, NaN) is true and Object.is(0, -0) is false. React uses Object.is to decide whether state has changed, which is why setting state to the same object reference does not trigger a re-render.

Falsy values are: false, 0, -0, 0n, "", null, undefined, and NaN. Everything else, including [], {}, and "0", is truthy. This matters in JSX, where {items.length && <List />} renders a literal 0 when the array is empty. Prefer {items.length > 0 && <List />}.

Modern Syntax Toolkit

Destructuring, Defaults, Rest, and Spread

const { id, name: displayName = 'Anonymous', ...rest } = user;
const [first, , third = 0] = numbers;

function createPayment({ amount, currency = 'USD', memo } = {}) { /* ... */ }

const merged = { ...defaults, ...overrides };   // later keys win
const copy = [...original, newItem];

Spread performs a shallow copy. Nested objects are still shared, which matters when updating React state: you must spread at every level you change.

Default parameters apply only when the argument is undefined, not when it is null.

Optional Chaining and Nullish Coalescing

const city = order?.customer?.address?.city ?? 'Unknown';
const fee = config.fee ?? 0;   // keeps 0, unlike config.fee || 0
callback?.();                  // call only if defined

?? returns the right-hand side only for null or undefined. || returns it for any falsy value, which is a frequent bug when 0 or "" are valid values (an amount of zero, an empty search string).

Template Literals and Tagged Templates

Template literals support interpolation and multi-line strings. Tagged templates let a function process the parts, which libraries use for CSS-in-JS, GraphQL queries, and safe HTML escaping.

function html(strings, ...values) {
  return strings.reduce((out, str, i) =>
    out + str + (i < values.length ? escapeHtml(values[i]) : ''), '');
}
const markup = html`<p>${userInput}</p>`;

Arrow Functions

Arrow functions have no own this, arguments, super, or new.target, and cannot be used as constructors. They are ideal for callbacks and poor as object methods that need this.

Iterators, Generators, and for...of

An object is iterable if it has a [Symbol.iterator] method returning an iterator (an object with next()). Arrays, strings, maps, sets, and arguments are iterable. for...of, spread, destructuring, Array.from, and Promise.all all consume iterables.

function* paginate(items, size) {
  for (let i = 0; i < items.length; i += size) {
    yield items.slice(i, i + size);
  }
}
for (const page of paginate(transactions, 50)) render(page);

for...in iterates over enumerable string keys, including inherited ones; use it for objects only, and prefer Object.keys, Object.values, or Object.entries.

Map, Set, WeakMap, WeakSet

Map keeps insertion order, accepts any key type, and has an O(1) size. Prefer it over plain objects for dictionaries with dynamic keys. Set stores unique values and is the simplest way to de-duplicate: [...new Set(ids)].

WeakMap and WeakSet hold their keys weakly: if nothing else references a key object, it can be garbage collected along with its entry. They are not iterable. Use them to attach metadata to objects you do not own, such as caching computed values per DOM node.

Symbols

Symbols are unique, non-string property keys. Well-known symbols such as Symbol.iterator, Symbol.asyncIterator, and Symbol.toPrimitive let you hook into language behavior.

Array Methods Every Interviewer Expects

Method Returns Mutates?
map, filter, slice, concat, flat, flatMap New array No
reduce Accumulated value No
find, findIndex, findLast, findLastIndex Element or index No
some, every, includes Boolean No
push, pop, shift, unshift, splice, sort, reverse, fill Varies Yes
toSorted, toReversed, toSpliced, with (ES2023) New array No

The ES2023 non-mutating variants are welcome in React code, where mutating state arrays in place causes missed renders:

setRows(rows => rows.toSorted((a, b) => a.date.localeCompare(b.date)));

Array.prototype.sort sorts by string comparison by default, so [10, 9, 1].sort() gives [1, 10, 9]. Always pass a comparator for numbers. Since ES2019, sort is guaranteed to be stable.

Object.groupBy (ES2024) groups items by a key function:

const byStatus = Object.groupBy(payments, p => p.status);
// { pending: [...], settled: [...], failed: [...] }

Numbers, Money, and BigInt

JavaScript numbers are IEEE 754 doubles. 0.1 + 0.2 !== 0.3. Integers are exact only up to Number.MAX_SAFE_INTEGER (2^53 – 1). For financial applications this is a serious concern, covered in depth in the payments chapter. The short rules are:

  • Never do currency arithmetic with floating-point dollars. Use integer minor units (cents) or a decimal library.
  • Format with Intl.NumberFormat, never with string concatenation.
  • Use BigInt (123n) when you truly need arbitrarily large integers, such as 64-bit IDs from a backend. BigInt cannot be mixed with number in arithmetic and is not serialized by JSON.stringify without a replacer.

The Event Loop

JavaScript runs your code on a single thread. The event loop coordinates that thread with the outside world:

  1. Run the current synchronous script or task to completion (the call stack empties).
  2. Run all microtasks in the microtask queue, including ones added while draining it. Microtasks come from promise reactions (then, catch, finally, await continuations), queueMicrotask, and MutationObserver.
  3. If it is time, let the browser render (style, layout, paint), running requestAnimationFrame callbacks just before.
  4. Pick the next macrotask (timers, I/O, user events, MessageChannel messages) and repeat.
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
queueMicrotask(() => console.log('4'));
(async () => { console.log('5'); await null; console.log('6'); })();
console.log('7');
// 1, 5, 7, 3, 4, 6, 2

5 prints synchronously because an async function runs synchronously until its first await. Everything after await becomes a microtask. 2 is a macrotask, so it waits for the microtask queue to drain.

Two practical consequences:

  • A long synchronous loop blocks rendering and input. Break it up with scheduler.yield() where supported, setTimeout, or move it to a Web Worker.
  • An infinite microtask loop (a promise that always schedules another) freezes the page just like an infinite while loop, because rendering never gets a turn.

Promises

A promise represents a value that will be available later. It is in one of three states: pending, fulfilled, or rejected, and once settled it never changes.

function fetchJson(url) {
  return fetch(url).then(res => {
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  });
}

Rules worth stating in an interview:

  • then always returns a new promise. Returning a value fulfills it; throwing rejects it; returning a promise adopts that promise’s state.
  • A catch handles rejections from anything earlier in the chain, and returning normally from catch recovers the chain.
  • Forgetting to return a promise inside a then breaks the chain: the outer chain continues without waiting.
  • Unhandled rejections trigger the unhandledrejection event in browsers. Log them centrally.

Promise Combinators

Combinator Fulfills when Rejects when
Promise.all All fulfill (array of values) Any rejects (first reason)
Promise.allSettled All settle (array of {status, value/reason}) Never
Promise.race First settles, if it fulfills First settles, if it rejects
Promise.any First fulfills All reject (AggregateError)

Promise.withResolvers() (ES2024) returns { promise, resolve, reject }, which removes the boilerplate of capturing resolvers from a constructor.

Implementing Promise.all

A frequent whiteboard exercise:

function promiseAll(iterable) {
  return new Promise((resolve, reject) => {
    const items = Array.from(iterable);
    const results = new Array(items.length);
    let remaining = items.length;
    if (remaining === 0) return resolve(results);
    items.forEach((item, index) => {
      Promise.resolve(item).then(value => {
        results[index] = value;
        if (--remaining === 0) resolve(results);
      }, reject);
    });
  });
}

Note the details: wrapping each item with Promise.resolve handles non-promise values, results keep input order regardless of completion order, and the empty case resolves immediately.

async and await

async functions always return a promise. await pauses the function until the awaited promise settles, then resumes with the value or throws the reason.

async function loadDashboard(userId) {
  try {
    const [profile, accounts] = await Promise.all([
      api.getProfile(userId),
      api.getAccounts(userId),
    ]);
    return { profile, accounts };
  } catch (err) {
    logger.error('dashboard failed', err);
    throw err;
  }
}

Common mistakes:

  • Accidental serialization. Awaiting independent calls one after another doubles latency. Start them together with Promise.all.
  • await in forEach. forEach ignores returned promises, so the loop does not wait. Use for...of for sequential work or Promise.all(items.map(...)) for parallel work.
  • Swallowed errors. An async function called without await or .catch produces an unhandled rejection.
  • Top-level await is allowed in ES modules. It blocks the evaluation of modules that import the awaiting module, so use it sparingly in shared code.

Cancellation with AbortController

Promises cannot be cancelled, but the work behind them often can:

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
  const res = await fetch('/api/transactions', { signal: controller.signal });
  // ...
} catch (err) {
  if (err.name === 'AbortError') { /* cancelled or timed out */ }
} finally {
  clearTimeout(timeout);
}

AbortSignal.timeout(ms) and AbortSignal.any([...signals]) make this shorter in modern browsers. Axios accepts the same signal option.

Modules

ES modules (ESM) are the standard module system: import and export statements, statically analyzable, always strict, evaluated once, with live bindings.

// money.js
export const CURRENCY = 'USD';
export function formatCents(cents) { /* ... */ }
export default class Money { /* ... */ }

// app.js
import Money, { formatCents, CURRENCY as DEFAULT_CURRENCY } from './money.js';
const { heavyChart } = await import('./charts.js'); // dynamic import, code splitting

ESM vs. CommonJS (require/module.exports):

ESM CommonJS
Loading Asynchronous, static graph Synchronous, at runtime
Exports Live bindings Copied values (the exports object)
Tree shaking Yes, because imports are static Difficult
Top-level this undefined module.exports
Top-level await Yes No

Static imports are what make tree shaking possible and what Module Federation builds on when it shares modules between separately deployed applications.

Functional Patterns: Debounce, Throttle, Memoize

These three utilities come up constantly, both as coding exercises and in real UI work (search boxes, scroll handlers, expensive calculations).

function debounce(fn, wait) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), wait);
  };
}

function throttle(fn, interval) {
  let last = 0;
  let trailing;
  return function (...args) {
    const now = Date.now();
    const remaining = interval - (now - last);
    if (remaining <= 0) {
      last = now;
      fn.apply(this, args);
    } else {
      clearTimeout(trailing);
      trailing = setTimeout(() => {
        last = Date.now();
        fn.apply(this, args);
      }, remaining);
    }
  };
}

function memoize(fn, keyFn = (...args) => JSON.stringify(args)) {
  const cache = new Map();
  return (...args) => {
    const key = keyFn(...args);
    if (!cache.has(key)) cache.set(key, fn(...args));
    return cache.get(key);
  };
}

Debounce waits for a pause in calls; throttle guarantees at most one call per interval. Use debounce for search-as-you-type and window resize end; use throttle for scroll position tracking and rate-limited analytics.

Error Handling

Throw Error objects (or subclasses), never strings, so you get stack traces. Since ES2022, Error accepts a cause:

class PaymentError extends Error {
  constructor(message, { code, cause } = {}) {
    super(message, { cause });
    this.name = 'PaymentError';
    this.code = code;
  }
}
try {
  await api.submitPayment(payload);
} catch (err) {
  throw new PaymentError('Payment submission failed', { code: 'SUBMIT_FAILED', cause: err });
}

structuredClone(value) deep-copies most built-in types (dates, maps, sets, typed arrays, nested objects) and is the right answer to “how do you deep clone an object” in modern environments, unlike JSON.parse(JSON.stringify(x)), which drops undefined, functions, and dates.

Performance-Aware JavaScript

  • Avoid creating closures and objects in hot loops when not needed, but do not micro-optimize before measuring.
  • Prefer Map and Set for frequent lookups instead of Array.includes on large arrays.
  • Batch DOM reads and writes to avoid layout thrashing (reading offsetHeight after a style change forces a synchronous layout).
  • Use requestAnimationFrame for visual updates and requestIdleCallback for low-priority work.
  • Move CPU-heavy work (parsing large CSV exports, encryption, diffing) into a Web Worker.

Interview Questions and Answers

Q1. What is a closure, and give a practical use case?

A closure is a function bundled with references to the variables in the scope where it was created. It lets the function keep using those variables after the outer function has returned. Practical uses include data privacy (module pattern, counters), function factories (makeFormatter('USD')), memoization caches, debounced handlers that remember a timer ID, and every React hook, which relies on closures over the current render’s props and state.

Q2. Why does for (var i...) with setTimeout print the same number three times, and how do you fix it?

var is function-scoped, so all three callbacks close over the same i. When they run, the loop has finished and i equals 3. Fix it with let, which creates a new binding per iteration, or by passing i into an IIFE or as the third argument to setTimeout.

Q3. Explain the temporal dead zone.

let, const, and class declarations are hoisted to the top of their block but are not initialized. From the start of the block until the declaration executes, the binding is in the TDZ, and any access throws ReferenceError. It exists to catch use-before-declaration bugs that var silently allowed.

Q4. How is this determined?

By the call site. In precedence order: new binds a new object; call, apply, and bind set it explicitly; calling as obj.method() binds obj; a plain call binds undefined in strict mode or the global object in sloppy mode. Arrow functions ignore all of these and inherit this from their enclosing scope.

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

All three set this. call(thisArg, a, b) invokes immediately with individual arguments. apply(thisArg, [a, b]) invokes immediately with an array of arguments. bind(thisArg, a) returns a new function with this (and optionally leading arguments) permanently bound, without invoking it.

Q6. Explain prototypal inheritance and how class relates to it.

Objects delegate property lookups to their prototype via the [[Prototype]] link, forming a chain that ends in Object.prototype and then null. class is syntax over this model: methods go on Constructor.prototype, extends sets up the prototype chain of both instances and constructors, and super calls the parent’s methods. Classes add strict mode, private fields, static blocks, and protection against calling a constructor without new.

Q7. What is the difference between == and ===?

=== checks type and value without conversion. == converts operands using the abstract equality rules (for example, "1" == 1 is true, null == undefined is true, [] == false is true). Use === by default; the only common exception is x == null to test for both null and undefined.

Q8. Walk through the output of a mixed setTimeout, promise, and async snippet.

Synchronous code runs first, including the body of an async function up to its first await. Then the microtask queue drains completely: promise reactions and await continuations in the order they were queued. Only then does the next macrotask, such as a setTimeout callback, run. I would talk through each line, labeling it sync, micro, or macro, and order accordingly.

Q9. What are microtasks and macrotasks?

Macrotasks (tasks) are units of work such as script execution, timers, user events, and network callbacks; the event loop runs one per iteration. Microtasks are smaller jobs, mainly promise reactions and queueMicrotask callbacks, that run immediately after the current task finishes and before rendering. The microtask queue is drained completely each time, so microtasks can starve rendering if they keep scheduling more.

Q10. How would you implement Promise.all? What about Promise.allSettled?

For all, return a new promise; iterate the inputs, wrap each with Promise.resolve, store results by index, decrement a counter on fulfillment, resolve when the counter hits zero, and reject on the first rejection. Handle the empty input by resolving immediately. For allSettled, attach both handlers to each input, store {status: 'fulfilled', value} or {status: 'rejected', reason}, and resolve when all have settled; it never rejects.

Q11. When would you use Promise.allSettled instead of Promise.all?

When the operations are independent and partial success is useful. A dashboard that loads balances, recent transactions, and alerts should show whatever succeeded and display errors for the rest, rather than failing entirely because one widget’s request failed.

Q12. What is wrong with items.forEach(async item => await save(item))?

forEach ignores the promises returned by the callback, so the surrounding code does not wait for the saves and errors become unhandled rejections. Use for (const item of items) await save(item) for sequential saves or await Promise.all(items.map(save)) for parallel saves, possibly with a concurrency limit.

Q13. How do you cancel a fetch request?

Create an AbortController, pass controller.signal to fetch (or Axios), and call controller.abort(). The promise rejects with an AbortError, which you should treat as a non-error in UI code. In React effects, abort in the cleanup function so a response from an outdated request never updates state.

Q14. Explain debounce versus throttle and implement one.

Debounce delays execution until calls stop for a given wait period; only the last call in a burst runs. Throttle executes at most once per interval during continuous calls. A debounce implementation clears and resets a timer on every call, then invokes the function with the latest arguments and this when the timer fires (see the implementation earlier in the chapter).

Q15. What is the difference between shallow and deep copy? How do you deep copy?

A shallow copy duplicates the top-level properties; nested objects are still shared references. Spread and Object.assign are shallow. For deep copies use structuredClone, which supports dates, maps, sets, cyclic references, and typed arrays but not functions or DOM nodes. JSON.parse(JSON.stringify(x)) is a lossy fallback.

Q16. What are WeakMap and WeakRef used for?

WeakMap associates data with objects without preventing those objects from being garbage collected, which makes it good for caches and private metadata keyed by objects. WeakRef holds a weak reference to a single object, and FinalizationRegistry runs a callback after collection; both are advanced tools, and their timing is non-deterministic, so they should not be used for program logic.

Q17. Explain ES modules versus CommonJS.

ESM uses static import/export, loads asynchronously, provides live bindings, supports top-level await, and enables tree shaking. CommonJS uses require at runtime, loads synchronously, exports a copied object, and is harder to tree shake. Bundlers can consume both, but mixing them causes interop issues around default exports.

Q18. What does "use strict" change?

It turns silent errors into thrown errors (assigning to undeclared variables, writing to read-only properties), makes this undefined in plain function calls, forbids duplicate parameter names and with, and reserves future keywords. Modules and classes are strict automatically.

Q19. Why does 0.1 + 0.2 !== 0.3, and how do you handle money?

Numbers are binary floating-point, and 0.1 and 0.2 have no exact binary representation, so their sum is 0.30000000000000004. For money, store and compute in integer minor units (cents) or use a decimal library, round only at defined points using a stated rounding mode, and format with Intl.NumberFormat.

Q20. What is optional chaining, and what are its limits?

a?.b returns undefined if a is null or undefined instead of throwing. It short-circuits the rest of the chain. It cannot be used on the left side of an assignment, and overusing it can hide data-shape bugs that should fail loudly. Combine it with ?? to provide defaults.

Q21. Explain generators and a real use.

A generator function (function*) returns an iterator; each yield pauses execution and produces a value, and the function resumes on the next next() call. Uses include lazy sequences, paginating large lists without building them all at once, custom iterables, and (historically) coroutine libraries such as redux-saga. Async generators with for await...of are useful for consuming streams, such as reading a streaming AI response chunk by chunk.

Q22. What is event delegation?

Attaching one listener to a common ancestor instead of many listeners to child elements, then using event.target (often with closest()) to work out which child was interacted with. It saves memory, handles dynamically added children, and is how React’s synthetic event system works internally: React 17+ attaches listeners at the root container.

Q23. What is the difference between null and undefined?

undefined means a value was never assigned: missing properties, uninitialized variables, missing arguments, functions without a return. null is an explicit “no value” assigned by a programmer. typeof null is "object", a historical bug. JSON has null but not undefined, so undefined properties disappear on serialization.

Q24. What is currying, and why would you use it?

Currying transforms a function of several arguments into a chain of functions each taking one argument: add(a)(b). It enables partial application and reusable specialized functions, like const formatUsd = formatCurrency('en-US')('USD'). In UI code it often appears as handler factories: onChange={handleField('email')}.

Q25. How do you find and fix a memory leak in a single-page application?

Reproduce the leak with a repeated user flow, take heap snapshots in Chrome DevTools before and after, and compare retained objects, looking for detached DOM nodes and growing arrays or maps. Common causes are listeners not removed, timers not cleared, subscriptions not cancelled on unmount, global caches without eviction, and closures holding large data. Fix by cleaning up in effect cleanup functions and bounding caches.

Q26. What are the newest language features you have used?

A strong answer names a few with a practical use: Array.prototype.toSorted and with for immutable state updates, Object.groupBy for grouping transactions, Promise.withResolvers for wrapping event-based APIs, structuredClone for deep copies, Array.prototype.at(-1) for the last element, the cause option on errors for error chaining, and Set methods such as union and intersection (ES2025) for permission checks.

Q27. What is the output of [1, 2, 3].map(parseInt)?

[1, NaN, NaN]. map passes (value, index, array), so the calls are parseInt('1', 0), parseInt('2', 1), and parseInt('3', 2). Radix 0 is treated as 10, radix 1 is invalid, and '3' is not a valid binary digit. The fix is map(Number) or map(s => parseInt(s, 10)).

Q28. How would you deep-compare two objects?

Check reference equality first, then type, then for arrays compare length and each element recursively, for plain objects compare key sets and each value recursively, and handle special cases such as dates (compare getTime()), NaN, maps, and sets. Protect against cycles with a WeakMap of visited pairs. In production, use a tested utility; in React, prefer designing state so that reference equality is enough.

Key Takeaways

  • Scope is lexical; this is dynamic (except for arrow functions).
  • Closures capture variables, not snapshots, which explains both their power and the stale-closure bug.
  • The event loop runs sync code, then all microtasks, then a render opportunity, then the next macrotask.
  • Promises chain by returning values; always return or await, and always handle rejection.
  • Use modern, non-mutating APIs for state updates, ?? for defaults, and AbortController for cancellation.
  • Never use floating-point numbers for money.

All chapters · Part 2: React 18 Fundamentals and Hooks →

Senior Frontend Interview Guide (full PDF)Every question and answer from this series in one printable guide.PDF · 12.5 MB
Free download: unlock it in one step

Share this page

or

Mailchimp opens in a new tab to confirm. Unsubscribe any time.

Filed underInterview Guide
Rupak Nepali
Written byRupak Nepali

Author of four Opencart book.
The recent are Opencart 4 developer book and Opencart 4 user manual

Leave a Reply

Your email address will not be published. Required fields are marked *