Javascript · Modern JS

Modern JS Interview Questions
With Answers & Code Examples

15 carefully curated Modern JS interview questions with working code examples and real interview gotchas.

Practice Interactively →← All Categories
15 questions0 beginner7 core8 advanced
Q1Advanced

What are generators and when would you use them?

💡 Hint: function* that can yield values lazily; returns an iterator

Generators (function*) can pause execution and yield values one at a time.

function* range(start, end) {
  for (let i = start; i <= end; i++) yield i;
}
for (const num of range(1, 5)) console.log(num); // 1,2,3,4,5

// Infinite lazy sequence
function* naturals() {
  let n = 0;
  while (true) yield n++;
}

Use cases: lazy/infinite sequences, custom iterators, Redux-Saga uses generators for async flows.

Practice this question →
Q2Core

What is optional chaining (?.) and nullish coalescing (??)?

💡 Hint: ?. short-circuits on null/undefined; ?? fallbacks only for null/undefined
// Optional chaining
const city = user?.address?.city;
const name = users?.[0]?.name;
const val = obj?.method?.();

// Nullish coalescing — fallback for null/undefined ONLY
const count = user.count ?? 0;
// if count=0, result is 0 (correct!)
// vs OR operator:
const bad = user.count || 0;
// if count=0, bad=0 but for wrong reason (0 is falsy)
💡 Use ?? when 0, '', false are valid values. Use || for general falsy fallbacks.
Practice this question →
Q3Core

What are tagged template literals and what are they used for?

💡 Hint: A function that processes the template — receives string parts and interpolated values separately

A tagged template is a function placed before a template literal — it receives the string parts and interpolated values separately, allowing custom processing.

function highlight(strings, ...values) {
  // strings: ['User ', ' scored ', '%']
  // values:  ['Alice', 95]
  return strings.reduce((result, str, i) =>
    result + str + (values[i] !== undefined
      ? `${values[i]}`
      : ''), '');
}

const user = 'Alice', score = 95;
highlight`User ${user} scored ${score}%`;
// 'User Alice scored 95%'

// Real-world uses:
// 1. styled-components
const Button = styled.div`
  background: ${props => props.primary ? 'blue' : 'white'};
`;

// 2. SQL sanitization (prevents injection!)
const result = sql`SELECT * FROM users WHERE id = ${userId}`;
// tag function escapes userId before inserting

// 3. GraphQL queries
const query = gql`
  query GetUser { user(id: ${id}) { name } }
`;
💡 Tagged templates are how styled-components and sql template libraries work. The tag function is called before string interpolation, enabling sanitization and custom processing.
Practice this question →
Q4Core

Explain destructuring for objects and arrays — including defaults, renaming, rest, and nesting.

💡 Hint: Extract values into variables with concise syntax — works in params, assignments, loops
// ── Array destructuring (position-based) ─────
const [a, b, c] = [1, 2, 3];
const [first, , third] = [1, 2, 3];    // skip index 1
const [x, ...rest] = [1, 2, 3, 4];     // x=1, rest=[2,3,4]
const [p = 10, q = 20] = [1];          // p=1, q=20 (default)

// ── Object destructuring (name-based) ────────
const { name, age } = { name: 'Alice', age: 25 };
const { name: userName } = { name: 'Alice' };  // rename to userName
const { city = 'NYC' } = {};                    // default if undefined

// ── Nested ────────────────────────────────────
const { address: { city: town } } = { address: { city: 'Paris' } };
// town = 'Paris'

// ── In function parameters ────────────────────
function greet({ name, age = 18, role = 'user' }) {
  return `${name} (age:${age}, ${role})`;
}

// ── Swap variables ────────────────────────────
let m = 1, n = 2;
[m, n] = [n, m]; // m=2, n=1

// ── In loops ─────────────────────────────────
for (const [key, value] of Object.entries(obj)) {
  console.log(key, value);
}
💡 Destructuring doesn't mutate the original — it creates new bindings. Combine default params + destructuring for clean, self-documenting function signatures.
Practice this question →
Q5Core

What are Symbols and what are their main use cases?

💡 Hint: Unique, non-string property keys — used for collision-free metadata and well-known protocols

A Symbol is a primitive that is guaranteed globally unique. Used mainly as object property keys to avoid name collisions.

const id = Symbol('id');
const id2 = Symbol('id');
id === id2; // false — always unique even with same description

const user = {};
user[id] = 42; // Symbol as property key

// Symbols are invisible to normal enumeration
Object.keys(user);                          // []
JSON.stringify(user);                       // '{}' — symbols excluded
Object.getOwnPropertySymbols(user);         // [Symbol(id)] — explicit access

// Well-known Symbols — customize built-in behavior
class MyIterable {
  [Symbol.iterator]() {      // makes instances work in for...of
    let n = 0;
    return { next: () => n < 3
      ? { value: n++, done: false }
      : { done: true } };
  }
}

for (const v of new MyIterable()) console.log(v); // 0, 1, 2

// Other well-known Symbols:
// Symbol.toPrimitive — control type coercion
// Symbol.hasInstance — customize instanceof
// Symbol.toStringTag — customize Object.prototype.toString output
💡 Use Symbols as property keys when extending objects you don't own — impossible to accidentally collide with existing or future string keys.
Practice this question →
Q6Core

What are Map and Set and how do they compare to objects and arrays?

💡 Hint: Map=ordered key-value with any key type; Set=unique-value collection; both iterable

Map vs plain object: keys can be any type, maintains insertion order, has .size, is directly iterable, better performance for frequent add/delete.

Set vs array: values must be unique, has O(1) lookup with .has(), no index access.

// Map
const map = new Map();
map.set('string', 1);
map.set(42, 'number key');     // any type as key!
map.set({}, 'object key');
map.get('string');  // 1
map.has(42);        // true
map.size;           // 3
map.delete(42);

// Iterate
for (const [k, v] of map) console.log(k, v);
[...map.keys()]; [...map.values()]; [...map.entries()];

// Convert to/from object
const obj = Object.fromEntries(map);
new Map(Object.entries(obj));

// Set
const set = new Set([1, 2, 2, 3, 3]); // {1, 2, 3} — duplicates removed
set.add(4);
set.has(2);   // true — O(1)
set.size;     // 4

// Remove duplicates from array (classic use)
const unique = [...new Set([1,2,2,3,3,3])]; // [1, 2, 3]

// Set operations
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
const union        = new Set([...a, ...b]);      // {1,2,3,4}
const intersection = new Set([...a].filter(x => b.has(x))); // {2,3}
💡 Use Map over objects when keys are non-strings, when insertion order matters, or when keys are frequently added/removed. Use Set for unique-value tracking.
Practice this question →
Q7Advanced

What are WeakMap and WeakSet and when do you use them?

💡 Hint: Keys are weakly held — objects can be GC'd; no iteration, no size — use for private metadata

WeakMap/WeakSet hold weak references to their keys/entries. The garbage collector can collect the referenced object — the entry is automatically removed. Keys must be objects.

// WeakMap — per-object cache that doesn't prevent GC
const cache = new WeakMap();

function process(user) {
  if (cache.has(user)) return cache.get(user); // cache hit
  const result = expensiveCompute(user);
  cache.set(user, result);
  return result;
}
// When user object is GC'd → cache entry vanishes automatically
// No manual cleanup needed!

// WeakSet — track objects without preventing GC
const processing = new WeakSet();
async function handleOnce(obj) {
  if (processing.has(obj)) return; // already running
  processing.add(obj);
  await doWork(obj);
  processing.delete(obj);
}

// WeakMap for private class fields (pre-#private syntax)
const _private = new WeakMap();
class Secure {
  constructor() { _private.set(this, { secret: 42 }); }
  getSecret() { return _private.get(this).secret; }
}

Key limitation: No .size, no iteration, no .keys()/.values(). You can't see what's in them — only access by key.

💡 WeakMap is perfect for: memoization caches keyed by object identity, private object metadata, DOM element data. The key insight: it doesn't extend the lifetime of the key object.
Practice this question →
Q8Advanced

What is Proxy and how does it enable metaprogramming?

💡 Hint: Intercept fundamental object operations (get, set, has, deleteProperty) with handler traps

A Proxy wraps an object and intercepts fundamental operations using "trap" handler methods.

const handler = {
  get(target, prop, receiver) {
    console.log(`Getting: ${prop}`);
    return Reflect.get(target, prop, receiver); // ← always use Reflect
  },
  set(target, prop, value, receiver) {
    if (typeof value !== 'number') throw new TypeError('Numbers only');
    return Reflect.set(target, prop, value, receiver);
  },
  has(target, prop) {
    return prop in target; // intercepts 'in' operator
  },
  deleteProperty(target, prop) {
    if (prop.startsWith('_')) throw new Error('Cannot delete private');
    return Reflect.deleteProperty(target, prop);
  }
};

const obj = new Proxy({}, handler);
obj.x = 42;
obj.x;         // logs "Getting: x" → 42
'x' in obj;    // calls has trap
obj.y = 'str'; // TypeError

// Real-world use cases:
// 1. Validation
// 2. Reactive state (Vue 3 uses Proxy for reactivity!)
// 3. Default property values
// 4. Logging / debugging
// 5. Negative array indexing
const arr = new Proxy([], {
  get: (t, p) => t[p < 0 ? t.length + +p : p]
});
arr[-1]; // last element
💡 Always use Reflect inside Proxy traps — it handles edge cases with prototype chains and getters/setters correctly. Reflect methods mirror Proxy trap signatures exactly.
Practice this question →
Q9Core

What are Iterators and Iterables in JavaScript?

💡 Hint: Iterator has next() → {value, done}. Iterable has [Symbol.iterator](). Used by for...of, spread, destructuring.

The iteration protocol standardizes how values are produced sequentially.

  • Iterator: an object with next() that returns { value, done }
  • Iterable: an object with [Symbol.iterator]() that returns an iterator
// Custom iterable range object
const range = {
  from: 1, to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      }
    };
  }
};

for (const n of range) console.log(n); // 1, 2, 3, 4, 5
[...range];   // [1, 2, 3, 4, 5]
const [a, b] = range; // destructuring works too

// Built-in iterables: Array, String, Map, Set, NodeList, arguments
for (const char of 'hello') console.log(char); // h, e, l, l, o

// Manual iteration
const iter = [1, 2][Symbol.iterator]();
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 2, done: false }
iter.next(); // { value: undefined, done: true }
💡 Anything that works with for...of, spread (...), or destructuring must be iterable. Implement [Symbol.iterator] to make your custom classes work with all these.
Practice this question →
Q10Advanced

What are ES Modules and how do they differ from CommonJS?

💡 Hint: ESM: static import/export, live bindings, always strict; CJS: require(), dynamic, copied values
// ── ES Modules (ESM) ─────────────────────────────
// math.js
export const PI = 3.14;
export function add(a, b) { return a + b; }
export default class App {}

// main.js
import { PI, add } from './math.js';   // named imports
import App from './App.js';             // default import
import * as Math from './math.js';      // namespace

// ── CommonJS (CJS) ─────────────────────────────
module.exports = { PI, add };
const { PI, add } = require('./math');

// ── Key Differences ────────────────────────────
FeatureESMCommonJS
AnalysisStatic (parse time)Dynamic (runtime)
BindingLive (tracks changes)Copied (snapshot)
Strict modeAlwaysOpt-in
Top-level await
Browsertype="module"Bundler needed
Tree shaking✅ (static)❌ (dynamic)
💡 Tree-shaking only works with ESM because imports are static — bundlers can analyze what's used. CJS require() is dynamic so bundlers can't eliminate dead code.
Practice this question →
Q11Core

What are dynamic imports and why are they useful?

💡 Hint: import() returns a Promise — enables code splitting, conditional loading, lazy loading

Dynamic imports (import()) load modules on demand, returning a Promise. Enables code splitting.

// Static — always loads at startup
import { parse } from 'csv-parser';

// Dynamic — loads only when needed
async function handleUpload(file) {
  if (file.type === 'text/csv') {
    const { parse } = await import('csv-parser'); // loaded now
    return parse(file);
  }
}

// React code splitting
const Chart = React.lazy(() => import('./HeavyChart'));

// Conditional loading
const lang = navigator.language;
const { messages } = await import(`./i18n/${lang}.js`); // dynamic path

// User-triggered loading
button.onclick = async () => {
  const { default: Editor } = await import('./editor.js');
  new Editor(container);
};

// Access default + named exports
const mod = await import('./math.js');
mod.default; // default export
mod.add;     // named export

Benefits: Smaller initial bundle, faster page load, load features only when needed.

💡 Webpack and Vite automatically create separate JS chunks for each dynamic import. Use loading states while the chunk loads.
Practice this question →
Q12Advanced

What are WeakRef and FinalizationRegistry?

💡 Hint: WeakRef: hold object without preventing GC; FinalizationRegistry: callback when object is collected

Advanced memory management APIs — use sparingly and only for performance optimizations.

// WeakRef — holds a weak reference (doesn't prevent GC)
let bigObject = { data: new Array(1_000_000).fill('*') };
const ref = new WeakRef(bigObject);

bigObject = null; // release strong reference → GC can now collect it

// Access via .deref() — returns undefined if already collected
const obj = ref.deref();
if (obj) {
  console.log('Still alive:', obj.data.length);
} else {
  console.log('Was garbage collected');
}

// FinalizationRegistry — callback when a registered object is collected
const registry = new FinalizationRegistry((heldValue) => {
  console.log('Collected! Clean up:', heldValue);
  cleanupResources(heldValue);
});

let target = { name: 'Alice' };
registry.register(target, 'alice-cleanup-token');
// target can now be GC'd — callback fires sometime after

Important: GC timing is non-deterministic. Don't use these for program correctness — only for optional caching or cleanup of non-critical resources.

💡 WeakRef + FinalizationRegistry are for library authors building caches that should automatically clean up. App code rarely needs these — use WeakMap instead.
Practice this question →
Q13Advanced

What are Async Generators and Async Iterators?

💡 Hint: function* + async = yield Promises lazily; consumed with for await...of

Async generators combine generator syntax with async/await — they yield values asynchronously and are consumed with for await...of.

// Async generator — paginated API fetcher
async function* fetchPages(url) {
  let page = 1;
  while (true) {
    const res = await fetch(`${url}?page=${page++}`);
    const { items, hasMore } = await res.json();
    yield items; // pause, give back items, resume on next iteration
    if (!hasMore) return; // done
  }
}

// Consume with for await...of
async function loadAll() {
  const allItems = [];
  for await (const items of fetchPages('/api/data')) {
    allItems.push(...items);
    if (allItems.length >= 100) break; // can stop early!
  }
  return allItems;
}

// Streaming data (Fetch Streams API)
async function* streamLines(url) {
  const reader = (await fetch(url)).body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  while (true) {
    const { done, value } = await reader.read();
    if (done) { if (buffer) yield buffer; return; }
    buffer += decoder.decode(value);
    const lines = buffer.split('\n');
    buffer = lines.pop();
    for (const line of lines) yield line;
  }
}
💡 Async generators are perfect for paginated APIs, event streams, or any sequence of values that arrive asynchronously over time.
Practice this question →
Q14Advanced

What is Reflect and how does it relate to Proxy?

💡 Hint: Reflect mirrors Proxy trap methods — use Reflect inside traps for correct default behavior

Reflect is a built-in object with static methods mirroring Proxy traps — same names, same signatures.

// Reflect mirrors operations but with better API design:
Reflect.get(target, prop, receiver);    // target[prop]
Reflect.set(target, prop, value, recv); // target[prop] = value
Reflect.has(target, prop);              // prop in target
Reflect.deleteProperty(target, prop);  // delete target[prop]
Reflect.ownKeys(target);               // all own keys (strings + symbols)
Reflect.apply(fn, thisArg, args);      // fn.apply(thisArg, args)
Reflect.construct(Cls, args);          // new Cls(...args)

// Why Reflect inside Proxy traps?
const proxy = new Proxy(obj, {
  get(target, prop, receiver) {
    log(prop);
    // Use Reflect.get (not target[prop]) to:
    // 1. Correctly pass receiver (preserves 'this' for getters)
    // 2. Return consistent boolean values
    return Reflect.get(target, prop, receiver); // ✅
    // return target[prop]; // ❌ breaks for inherited getters
  }
});

// Reflect.set returns true/false instead of throwing
const success = Reflect.set(obj, 'x', 5);
if (!success) console.log('Could not set');
💡 Rule: always use Reflect inside Proxy traps. It handles prototype chain, getters/setters, and non-writable properties correctly. Never do target[prop] directly in get trap.
Practice this question →
Q15Advanced

What are the basics of regular expressions in JavaScript?

💡 Hint: Pattern matching: literal chars, character classes, quantifiers, groups, flags (g, i, m)
// Creating regex
const r1 = /hello/;           // literal
const r2 = new RegExp('hello'); // dynamic pattern

// Test & match
/hello/.test('say hello');  // true
'hello world'.match(/\w+/g); // ['hello', 'world']
'hello world'.match(/(\w+)\s(\w+)/); // with groups

// Character classes
/[aeiou]/     // any vowel
/[^aeiou]/    // NOT a vowel
/[a-z]/       // lowercase letter
/\d/          // digit [0-9]
/\w/          // word char [a-zA-Z0-9_]
/\s/          // whitespace
/./           // any char except newline

// Quantifiers
/a+/          // one or more
/a*/          // zero or more
/a?/          // zero or one
/a{3}/        // exactly 3
/a{2,5}/      // 2 to 5

// Anchors
/^hello/      // starts with
/world$/      // ends with

// Groups
/(\d{4})-(\d{2})-(\d{2})/.exec('2024-01-15');
// groups: ['2024', '01', '15']
/(?<year>\d{4})-(?<month>\d{2})/.exec('2024-01');
// Named: match.groups.year, match.groups.month

// Lookahead / lookbehind
/\d+(?= dollars)/   // digits followed by " dollars"
/(?<=\$)\d+/        // digits preceded by $
/\d+(?! dollars)/   // digits NOT followed by " dollars"

// Flags
/pattern/g   // global — find all matches
/pattern/i   // case insensitive
/pattern/m   // multiline — ^ and $ match line starts/ends
/pattern/s   // dotAll — . matches newline too

// Common operations
'a1b2'.replace(/\d/g, '#');    // 'a#b#'
'a,b,,c'.split(/,+/);          // ['a','b','c']
'hello'.search(/e/);            // 1 (index)
💡 Greedy vs lazy: /a.*b/ greedily matches as MUCH as possible; /a.*?b/ lazily matches as LITTLE as possible. Add ? after quantifiers for lazy: +?, *?, {n,m}?
Practice this question →

Other Javascript Interview Topics

Rendering StrategiesCore JSType SystemReact FundamentalsFunctionsMicrofrontendsGenericsAsync JSHooksObjectsMonorepoArrays'this' KeywordUtility TypesError HandlingBundle OptimizationPerformanceDOM & EventsState ManagementClasses & OOPCaching StrategiesComponent PatternsAdvanced TypesAuthenticationReact RouterFormsAdvanced PatternsFrontend SecurityConcurrent ReactServer ComponentsTestingEcosystemNetwork OptimizationCore Web VitalsBrowser APIs

Ready to practice Modern JS?

Get AI feedback on your answers, predict code output, and fix real bugs.

Start Free Practice →