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 questions3 beginner4 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 →
Q2Beginner

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 →
Q3Beginner

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 →
Q4Beginner

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

Core JSFunctionsAsync JSObjectsArrays'this' KeywordError HandlingPerformanceDOM & EventsBrowser APIs

Ready to practice Modern JS?

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

Start Free Practice →