SEO pillar page

JavaScript Interview Questions: The Complete 2026 Guide for All Levels

A complete JavaScript interview preparation guide with 55+ questions, deep answers, section-wise live coding challenges, and practical explanations for freshers through senior engineers.

55+ interview questionsLive challenge in every sectionFreshers to senior engineers

Author

Aarav Sharma

Senior JavaScript Interview Mentor

Aarav focuses on JavaScript interview preparation, frontend architecture, and helping developers turn tricky concepts into clear mental models for real interviews.

TL;DR

What this guide covers

55+ questions organized by seniority: Fundamentals → Functions → Async → ES6+ → Advanced → DOM → Coding Challenges
Covers every topic that actually shows up in real interviews - data types, closures, the event loop, prototypal inheritance, ES2025 features, and more
Every answer has a plain-English explanation plus a real code example you can run right now
Designed for freshers through senior engineers - jump to the section that matches your level
Practice every concept live in the browser with AI-powered mock interviews that give you instant feedback

If you're prepping for a JavaScript interview, you're in the right place. This guide covers every major topic - from the basic javascript interview questions freshers get asked on day one, to the senior javascript developer interview questions that trip up engineers with years of experience. We've organized everything by seniority level and topic, so you can go straight to what you need.

No filler. Every question is one an interviewer has actually asked.


Section

Fundamentals

Target: Freshers / 0–1 year experience

These are the js interview questions you'll face in almost every screening call, regardless of company size. Get these cold.

What are the different data types in JavaScript?

JavaScript has 8 data types, split into two categories.

Primitive types (stored by value):

  • String - text, e.g. "hello"

  • Number - integers and floats, e.g. 42, 3.14

  • BigInt - integers beyond Number.MAX_SAFE_INTEGER, e.g. 9007199254740993n

  • Boolean - true or false

  • undefined - a declared variable with no assigned value

  • null - an intentional absence of value

  • Symbol - a unique, anonymous identifier (ES6+)

Non-primitive (stored by reference):

  • Object - includes plain objects, arrays, functions, dates, etc.
typeof "hello"       // "string"
typeof 42            // "number"
typeof true          // "boolean"
typeof undefined     // "undefined"
typeof null          // "object"  ← famous bug, not fixable without breaking the web
typeof Symbol()      // "symbol"
typeof 42n           // "bigint"
typeof {}            // "object"
typeof function(){}  // "function"

The typeof null === "object" quirk has been in the language since 1995. It's a known bug that was never fixed because changing it would break millions of existing sites.


What is hoisting in JavaScript?

Hoisting is JavaScript's behavior of moving variable and function declarations to the top of their scope before code executes. Only the declaration is hoisted - not the initialization.

// var is hoisted and initialized to undefined
console.log(name); // undefined (not an error)
var name = "Alice";

// let/const are hoisted but NOT initialized - "temporal dead zone"
console.log(age); // ReferenceError: Cannot access 'age' before initialization
let age = 30;

// Function declarations are fully hoisted
greet(); // "Hello!" - works fine
function greet() {
  console.log("Hello!");
}

// Function expressions are NOT fully hoisted
sayBye(); // TypeError: sayBye is not a function
var sayBye = function() { console.log("Bye!"); };

The practical rule: always declare variables at the top of their scope, and prefer let/const over var to avoid hoisting surprises.


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

== is the loose equality operator - it performs type coercion before comparing. === is the strict equality operator - it compares both value and type, no coercion.

0 == false      // true  (false coerces to 0)
0 === false     // false (number vs boolean)

null == undefined  // true
null === undefined // false

"5" == 5        // true  (string coerces to number)
"5" === 5       // false

NaN == NaN      // false (NaN is never equal to anything, including itself)

Always use === unless you have a specific reason not to. The one common exception: x == null catches both null and undefined in one check.


What is the difference between var, let, and const?

This is one of the most common basic javascript interview questions. Three keywords, three different behaviors.

var

let

const

Scope

Function

Block

Block

Hoisted

Yes (as undefined)

Yes (TDZ)

Yes (TDZ)

Re-declarable

Yes

No

No

Re-assignable

Yes

Yes

No

// var leaks out of blocks
if (true) {
  var x = 1;
  let y = 2;
}
console.log(x); // 1
console.log(y); // ReferenceError

// const prevents reassignment, but not mutation
const arr = [1, 2, 3];
arr.push(4);       // fine - mutation is allowed
arr = [5, 6, 7];   // TypeError - reassignment is not

Use const by default. Use let when you need to reassign. Avoid var in modern code.


What is implicit type coercion?

Type coercion is JavaScript's automatic conversion of a value from one type to another. It happens silently, which is why it causes so many bugs.

// String coercion with +
"3" + 4    // "34" (number coerces to string)
3 + "4"    // "34"

// Numeric coercion with - * /
"5" - 2    // 3  (string coerces to number)
"5" * "2"  // 10

// Boolean coercion - falsy values:
// false, 0, "", null, undefined, NaN, 0n
Boolean(0)         // false
Boolean("")        // false
Boolean(null)      // false
Boolean([])        // true  (empty array is truthy!)
Boolean({})        // true  (empty object is truthy!)

The + operator is the sneaky one - it prefers string concatenation when either operand is a string. Every other arithmetic operator coerces to number.


What is the difference between null and undefined?

Both represent "no value," but they mean different things.

undefined - the language's default "nothing." A variable that's been declared but not assigned, a function with no return statement, a missing object property - all return undefined.

null - a deliberate, programmer-assigned "nothing." You set something to null on purpose to signal "this intentionally has no value."

let a;
console.log(a);           // undefined (declared, not assigned)
console.log(typeof a);    // "undefined"

let b = null;
console.log(b);           // null (intentionally empty)
console.log(typeof b);    // "object" (the famous bug)

// Checking for both
function isEmpty(val) {
  return val == null; // true for both null and undefined
}

What is NaN and how do you check for it?

NaN stands for "Not-a-Number." It's the result of a failed numeric operation - like trying to parse a non-numeric string or doing 0/0. The weird part: typeof NaN === "number". And NaN !== NaN - it's the only value in JavaScript not equal to itself.

parseInt("hello")  // NaN
0 / 0              // NaN
Math.sqrt(-1)      // NaN

typeof NaN         // "number" (yes, really)
NaN === NaN        // false

// Checking for NaN
isNaN("hello")     // true  (coerces to number first - unreliable)
isNaN(undefined)   // true  (coerces to NaN)

Number.isNaN("hello")  // false (no coercion - correct)
Number.isNaN(NaN)      // true  ← use this

Always use Number.isNaN(), not the global isNaN(). The global version coerces its argument first, giving you false positives.


What is strict mode?

Strict mode ("use strict") is an opt-in mode that makes JavaScript throw errors for things it would otherwise silently ignore. It was introduced in ES5 to help developers write cleaner, safer code.

"use strict";

x = 10;              // ReferenceError: x is not defined (no implicit globals)
delete Object.prototype; // TypeError
function f(a, a) {}  // SyntaxError: duplicate parameter names

// In strict mode, this is undefined in regular functions (not global object)
function show() {
  console.log(this); // undefined (not window)
}
show();

All ES6 modules run in strict mode automatically. If you're writing modern JavaScript with import/export, you're already in strict mode.


What is scope and the scope chain?

Scope determines where variables are accessible. JavaScript has three scope types: global, function, and block.

The scope chain is how JavaScript resolves variable names - it starts at the current scope and walks outward until it finds the variable or hits the global scope.

const global = "I'm global";

function outer() {
  const outerVar = "I'm in outer";

  function inner() {
    const innerVar = "I'm in inner";
    // inner can access all three: innerVar, outerVar, global
    console.log(global);   // "I'm global"
    console.log(outerVar); // "I'm in outer"
    console.log(innerVar); // "I'm in inner"
  }

  inner();
  // outer cannot access innerVar
  // console.log(innerVar); // ReferenceError
}

outer();

If a variable isn't found anywhere in the chain, you get a ReferenceError. This chain lookup is what makes closures possible.


What is a closure?

A closure is a function that remembers the variables from its outer scope even after that outer function has finished executing. It's one of the most powerful - and most tested - concepts in JavaScript.

function makeCounter() {
  let count = 0; // this variable is "closed over"

  return {
    increment() { count++; },
    decrement() { count--; },
    getCount()  { return count; }
  };
}

const counter = makeCounter();
counter.increment();
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getCount()); // 2

// count is private - you can't access it directly
console.log(counter.count); // undefined

Closures are how you create private state in JavaScript without classes. They're also the foundation of patterns like module pattern, partial application, and memoization.


Live challenge
Easy

Falsy Value Counter

Return how many values in the input array are falsy in JavaScript.

Remember the standard falsy values: false, 0, "", null, undefined, NaN, and 0n.

0/2 tests passing

Section

Functions & Execution Context

Target: Junior to mid-level

How does the this keyword work?

this refers to the object that's calling the function - not where the function is defined, but how it's invoked. That distinction trips up a lot of developers.

// Method call: this = the object
const user = {
  name: "Alice",
  greet() {
    console.log(`Hello, ${this.name}`);
  }
};
user.greet(); // "Hello, Alice"

// Regular function call: this = global (or undefined in strict mode)
const greet = user.greet;
greet(); // "Hello, undefined" (this is window/global, not user)

// Arrow function: this = lexical (inherited from enclosing scope)
const obj = {
  name: "Bob",
  greet: () => {
    console.log(this.name); // undefined - arrow functions don't have their own this
  }
};

// new: this = the newly created object
function Person(name) {
  this.name = name;
}
const p = new Person("Carol");
console.log(p.name); // "Carol"

The rule of thumb: look at what's to the left of the dot when the function is called. That's this. No dot? It's the global object (or undefined in strict mode).


What is the difference between call(), apply(), and bind()?

All three let you explicitly set this. The difference is in how they're invoked and how arguments are passed.

function introduce(greeting, punctuation) {
  return `${greeting}, I'm ${this.name}${punctuation}`;
}

const person = { name: "Dana" };

// call: invoke immediately, args as comma-separated list
introduce.call(person, "Hi", "!");      // "Hi, I'm Dana!"

// apply: invoke immediately, args as array
introduce.apply(person, ["Hey", "."]);  // "Hey, I'm Dana."

// bind: returns a NEW function with this permanently bound
const boundIntro = introduce.bind(person, "Hello");
boundIntro("?");  // "Hello, I'm Dana?"

Memory trick: Call = Comma-separated, Apply = Array. bind = Bound later.


What is an IIFE?

An IIFE (Immediately Invoked Function Expression, pronounced "iffy") is a function that runs the moment it's defined. The classic pattern for creating an isolated scope.

(function() {
  const secret = "I'm private";
  console.log(secret); // "I'm private"
})();

// console.log(secret); // ReferenceError - not accessible outside

// Modern equivalent using a block + let/const
{
  const secret = "Also private";
  console.log(secret);
}
// secret is gone here too

IIFEs were the standard way to avoid polluting the global scope before ES6 modules. You'll still see them in older codebases and bundler output.


What is the difference between arrow functions and regular functions?

Arrow functions aren't just shorter syntax - they behave fundamentally differently in several ways.

// 1. this binding
function RegularTimer() {
  this.seconds = 0;
  setInterval(function() {
    this.seconds++; // 'this' is window/undefined - broken!
  }, 1000);
}

function ArrowTimer() {
  this.seconds = 0;
  setInterval(() => {
    this.seconds++; // 'this' is the ArrowTimer instance - correct!
  }, 1000);
}

// 2. Arrow functions have no 'arguments' object
function regular() {
  console.log(arguments); // Arguments [1, 2, 3]
}
const arrow = () => {
  console.log(arguments); // ReferenceError
};

// 3. Arrow functions cannot be used as constructors
const Foo = () => {};
new Foo(); // TypeError: Foo is not a constructor

// 4. Arrow functions have no prototype property
console.log(Foo.prototype); // undefined

Use arrow functions for callbacks and short expressions. Use regular functions when you need this binding, arguments, or a constructor.


What are higher-order functions?

A higher-order function is a function that either takes another function as an argument, returns a function, or both. They're fundamental to functional programming in JavaScript.

// Takes a function as argument
function repeat(n, action) {
  for (let i = 0; i < n; i++) action(i);
}
repeat(3, console.log); // 0, 1, 2

// Returns a function
function multiplier(factor) {
  return (number) => number * factor;
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5));  // 10
console.log(triple(5));  // 15

// Built-in HOFs: map, filter, reduce
const numbers = [1, 2, 3, 4, 5];
const evens = numbers.filter(n => n % 2 === 0);    // [2, 4]
const doubled = numbers.map(n => n * 2);            // [2, 4, 6, 8, 10]
const sum = numbers.reduce((acc, n) => acc + n, 0); // 15

What is currying?

Currying transforms a function that takes multiple arguments into a chain of functions that each take one argument. Named after mathematician Haskell Curry.

// Non-curried
function add(a, b, c) {
  return a + b + c;
}
add(1, 2, 3); // 6

// Curried version
function curriedAdd(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}
curriedAdd(1)(2)(3); // 6

// Practical use: partial application
const add10 = curriedAdd(10);
const add10and5 = add10(5);
console.log(add10and5(3)); // 18

// Generic curry utility
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args);
    }
    return (...moreArgs) => curried(...args, ...moreArgs);
  };
}

const curriedMultiply = curry((a, b, c) => a * b * c);
curriedMultiply(2)(3)(4);   // 24
curriedMultiply(2, 3)(4);   // 24
curriedMultiply(2)(3, 4);   // 24

Currying shines when you want to create specialized functions from general ones - like a log(level)(message) function where you pre-bind the log level.


What is memoization?

Memoization is a caching technique where a function stores the results of previous calls and returns the cached result when called again with the same arguments. It trades memory for speed.

function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      console.log("Cache hit!");
      return cache.get(key);
    }
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Expensive Fibonacci without memoization: O(2^n)
function fib(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}

const memoFib = memoize(function fib(n) {
  if (n <= 1) return n;
  return memoFib(n - 1) + memoFib(n - 2);
});

console.time("first");
memoFib(40); // slow first time
console.timeEnd("first");

console.time("second");
memoFib(40); // instant - from cache
console.timeEnd("second");

What is the event loop (basics)?

JavaScript is single-threaded - it can only do one thing at a time. The event loop is the mechanism that lets it handle asynchronous operations without blocking.

console.log("1 - synchronous");

setTimeout(() => console.log("2 - macrotask"), 0);

Promise.resolve().then(() => console.log("3 - microtask"));

console.log("4 - synchronous");

// Output order:
// 1 - synchronous
// 4 - synchronous
// 3 - microtask   ← microtasks run before macrotasks
// 2 - macrotask

The call stack runs synchronous code. When it's empty, the event loop first drains the microtask queue (Promises, queueMicrotask), then picks one task from the macrotask queue (setTimeout, setInterval, I/O). Repeat.


💡 Ready to practice these concepts live? jsinterview.in's AI mock interview engine lets you answer questions out loud, run code in the browser, and get instant feedback on your explanation - just like a real interview. [Start your personalized practice session →]


Section

Asynchronous JavaScript

Target: Mid-level

What are callbacks and what is callback hell?

A callback is a function passed as an argument to another function, to be executed after some operation completes. They were the original async pattern in JavaScript.

// Simple callback
function fetchUser(id, callback) {
  setTimeout(() => {
    callback(null, { id, name: "Alice" }); // error-first convention
  }, 1000);
}

fetchUser(1, (err, user) => {
  if (err) return console.error(err);
  console.log(user); // { id: 1, name: "Alice" }
});

// Callback hell - deeply nested, hard to read and maintain
fetchUser(1, (err, user) => {
  fetchOrders(user.id, (err, orders) => {
    fetchOrderDetails(orders[0].id, (err, details) => {
      fetchShipping(details.shippingId, (err, shipping) => {
        // You get the idea...
      });
    });
  });
});

Callback hell (also called the "pyramid of doom") is why Promises were introduced in ES6.


What are Promises and how do they work?

A Promise represents the eventual result of an asynchronous operation. It's in one of three states: pending, fulfilled, or rejected. Once settled, it never changes state.

// Creating a Promise
const fetchData = (id) => new Promise((resolve, reject) => {
  setTimeout(() => {
    if (id > 0) {
      resolve({ id, name: "Alice" });
    } else {
      reject(new Error("Invalid ID"));
    }
  }, 1000);
});

// Consuming with .then/.catch/.finally
fetchData(1)
  .then(user => {
    console.log(user); // { id: 1, name: "Alice" }
    return user.name;
  })
  .then(name => console.log(`Name: ${name}`))
  .catch(err => console.error(err.message))
  .finally(() => console.log("Done"));

// Promise chaining flattens the callback pyramid
fetchUser(1)
  .then(user => fetchOrders(user.id))
  .then(orders => fetchOrderDetails(orders[0].id))
  .then(details => fetchShipping(details.shippingId))
  .catch(err => console.error(err));

What is async/await?

async/await is syntactic sugar over Promises, introduced in ES2017. It lets you write asynchronous code that reads like synchronous code.

// Promise chain version
function loadUserData(id) {
  return fetchUser(id)
    .then(user => fetchOrders(user.id))
    .then(orders => ({ user, orders }))
    .catch(err => console.error(err));
}

// async/await version - same behavior, much cleaner
async function loadUserData(id) {
  try {
    const user = await fetchUser(id);
    const orders = await fetchOrders(user.id);
    return { user, orders };
  } catch (err) {
    console.error(err);
  }
}

// async functions always return a Promise
const result = loadUserData(1); // Promise
result.then(data => console.log(data));

// Top-level await (ES2022, in modules only)
const data = await loadUserData(1);

One common mistake: forgetting that await only pauses the current async function, not the entire program.


What is the difference between Promise.all and Promise.race?

Both accept an array of Promises. The difference is in what they resolve to.

const p1 = new Promise(resolve => setTimeout(() => resolve("fast"), 100));
const p2 = new Promise(resolve => setTimeout(() => resolve("medium"), 500));
const p3 = new Promise(resolve => setTimeout(() => resolve("slow"), 1000));

// Promise.all: resolves when ALL resolve, rejects if ANY rejects
Promise.all([p1, p2, p3])
  .then(results => console.log(results)); // ["fast", "medium", "slow"] after ~1000ms

// Promise.race: resolves/rejects as soon as the FIRST one settles
Promise.race([p1, p2, p3])
  .then(result => console.log(result)); // "fast" after ~100ms

// Promise.allSettled: resolves when all settle, never rejects
Promise.allSettled([p1, Promise.reject("oops"), p3])
  .then(results => console.log(results));
// [
//   { status: "fulfilled", value: "fast" },
//   { status: "rejected",  reason: "oops" },
//   { status: "fulfilled", value: "slow" }
// ]

// Promise.any: resolves with the FIRST fulfilled (ignores rejections)
Promise.any([Promise.reject("a"), p1, p2])
  .then(result => console.log(result)); // "fast"

Use Promise.all for parallel requests where you need all results. Use Promise.allSettled when you want results regardless of failures.


What is the microtask queue and how does it differ from the macrotask queue?

The event loop processes two types of async work: microtasks and macrotasks (also called tasks).

console.log("sync 1");

// Macrotask (setTimeout, setInterval, I/O, UI events)
setTimeout(() => console.log("macrotask"), 0);

// Microtask (Promise callbacks, queueMicrotask, MutationObserver)
Promise.resolve().then(() => console.log("microtask 1"));
Promise.resolve().then(() => console.log("microtask 2"));

queueMicrotask(() => console.log("microtask 3"));

console.log("sync 2");

// Output:
// sync 1
// sync 2
// microtask 1
// microtask 2
// microtask 3
// macrotask

The rule: after each macrotask, the engine drains the entire microtask queue before moving to the next macrotask. This means if a microtask queues another microtask, that new one also runs before any macrotask. Infinite microtask loops will starve the event loop.


What is the deep-dive event loop - call stack, Web APIs, and queues?

┌─────────────────────────────────────────────────────┐
│  JavaScript Engine                                   │
│  ┌──────────────┐    ┌──────────────────────────┐   │
│  │  Call Stack  │    │  Web APIs / Node APIs    │   │
│  │  (LIFO)      │    │  setTimeout, fetch, DOM  │   │
│  └──────┬───────┘    └──────────────┬───────────┘   │
│         │                           │               │
│  ┌──────▼───────────────────────────▼───────────┐   │
│  │  Event Loop                                  │   │
│  │  1. Run all sync code (call stack)           │   │
│  │  2. Drain microtask queue (Promises, etc.)   │   │
│  │  3. Run ONE macrotask (setTimeout, etc.)     │   │
│  │  4. Drain microtask queue again              │   │
│  │  5. Render (browser only)                   │   │
│  │  6. Repeat                                   │   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘
// Practical example: why setTimeout(fn, 0) isn't truly "0ms"
function blockFor(ms) {
  const start = Date.now();
  while (Date.now() - start < ms) {} // blocks the call stack
}

setTimeout(() => console.log("timer"), 0);
blockFor(500); // blocks for 500ms
// The timer callback runs AFTER blockFor finishes, not after 0ms
// Actual delay: ~500ms

What is the difference between setTimeout and setInterval?

Both schedule code to run asynchronously, but they differ in repetition.

// setTimeout: runs ONCE after the delay
const timeoutId = setTimeout(() => {
  console.log("Runs once after 1 second");
}, 1000);

clearTimeout(timeoutId); // cancel before it fires

// setInterval: runs REPEATEDLY every interval
let count = 0;
const intervalId = setInterval(() => {
  count++;
  console.log(`Tick ${count}`);
  if (count >= 3) clearInterval(intervalId); // stop after 3 ticks
}, 1000);

// Problem with setInterval: if the callback takes longer than the interval,
// calls can stack up. Recursive setTimeout is safer:
function safeInterval(fn, delay) {
  function tick() {
    fn();
    setTimeout(tick, delay); // next call only scheduled after current finishes
  }
  setTimeout(tick, delay);
}

How do you handle errors in async code?

// With Promises: .catch()
fetch("/api/data")
  .then(res => {
    if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
    return res.json();
  })
  .catch(err => console.error("Fetch failed:", err.message));

// With async/await: try/catch
async function getData() {
  try {
    const res = await fetch("/api/data");
    if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
    const data = await res.json();
    return data;
  } catch (err) {
    console.error("Failed:", err.message);
    throw err; // re-throw if caller needs to handle it
  }
}

// Handling errors in Promise.all
async function loadAll() {
  try {
    const [users, posts] = await Promise.all([
      fetch("/api/users").then(r => r.json()),
      fetch("/api/posts").then(r => r.json())
    ]);
    return { users, posts };
  } catch (err) {
    // If either fetch fails, we land here
    console.error(err);
  }
}

Always handle rejections. Unhandled promise rejections crash Node.js processes and generate browser warnings.


💡 Struggling with async concepts? Our AI mock interviewer will ask you to explain the event loop, then give you real-time feedback on your answer - including what you missed and how to improve. [Try an async JavaScript mock interview →]


Section

ES6+ & Modern JavaScript

Target: Mid to senior level

What is destructuring?

Destructuring lets you unpack values from arrays or properties from objects into distinct variables. It's one of the most-used ES6 features.

// Object destructuring
const user = { name: "Alice", age: 30, city: "London" };
const { name, age, city = "Unknown" } = user; // default value for city
console.log(name, age, city); // "Alice" 30 "London"

// Rename on destructure
const { name: userName } = user;
console.log(userName); // "Alice"

// Array destructuring
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first);  // 1
console.log(second); // 2
console.log(rest);   // [3, 4, 5]

// Swap variables
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1

// Function parameter destructuring
function greet({ name, age = 0 }) {
  return `${name} is ${age}`;
}
greet({ name: "Bob", age: 25 }); // "Bob is 25"

What is the difference between spread and rest operators?

They use the same ... syntax but do opposite things.

// SPREAD: expands an iterable into individual elements
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]

// Spread for object merging (shallow)
const defaults = { theme: "light", lang: "en" };
const userPrefs = { lang: "fr", fontSize: 14 };
const config = { ...defaults, ...userPrefs };
// { theme: "light", lang: "fr", fontSize: 14 }
// Note: userPrefs.lang overwrites defaults.lang

// Spread to copy (shallow)
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };
copy.a = 99;        // doesn't affect original
copy.b.c = 99;      // DOES affect original - shallow copy!

// REST: collects remaining elements into an array
function sum(first, second, ...others) {
  const total = others.reduce((acc, n) => acc + n, 0);
  return first + second + total;
}
sum(1, 2, 3, 4, 5); // 15

// Rest in destructuring
const { a, ...remaining } = { a: 1, b: 2, c: 3 };
console.log(remaining); // { b: 2, c: 3 }

What are template literals?

Template literals use backticks and support multi-line strings, embedded expressions, and tagged templates.

const name = "World";
const greeting = `Hello, ${name}!`; // "Hello, World!"

// Multi-line (no \n needed)
const html = `
  <div>
    <p>${greeting}</p>
  </div>
`;

// Any expression works inside ${}
const price = 9.99;
console.log(`Total: $${(price * 1.2).toFixed(2)}`); // "Total: $11.99"

// Tagged templates - advanced use case
function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    return result + str + (values[i] ? `<strong>${values[i]}</strong>` : "");
  }, "");
}

const product = "JavaScript";
const score = 10;
highlight`Learn ${product} and score ${score}/10`;
// "Learn <strong>JavaScript</strong> and score <strong>10</strong>/10"

What are ES6 modules (import/export)?

ES6 modules give JavaScript a native module system. Before this, developers used CommonJS (require/module.exports) in Node.js or AMD in browsers.

// math.js - named exports
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }

// Default export (one per module)
export default class Calculator { /* ... */ }

// app.js - importing
import Calculator, { PI, add, multiply } from "./math.js";
import * as math from "./math.js"; // namespace import

console.log(PI);           // 3.14159
console.log(add(2, 3));    // 5
console.log(math.multiply(4, 5)); // 20

// Dynamic import (lazy loading)
async function loadHeavyModule() {
  const { heavyFunction } = await import("./heavy.js");
  heavyFunction();
}

Key differences from CommonJS: ES modules are static (imports are resolved at parse time, not runtime), asynchronous by design, and always in strict mode.


What is optional chaining (?.)?

Optional chaining (?.) short-circuits and returns undefined instead of throwing a TypeError when accessing a property on null or undefined.

const user = {
  profile: {
    address: {
      city: "London"
    }
  }
};

// Without optional chaining - verbose and error-prone
const city = user && user.profile && user.profile.address && user.profile.address.city;

// With optional chaining - clean
const city2 = user?.profile?.address?.city; // "London"
const zip   = user?.profile?.address?.zip;  // undefined (no error)

// Works with methods and bracket notation too
const len = user?.getName?.();     // undefined if getName doesn't exist
const val = arr?.[0];              // undefined if arr is null/undefined

// Combine with nullish coalescing for defaults
const displayCity = user?.profile?.address?.city ?? "City not set";

What is nullish coalescing (??)?

The nullish coalescing operator (??) returns the right-hand side only when the left-hand side is null or undefined - not for other falsy values like 0, "", or false.

// The problem with ||
const count = 0;
console.log(count || 10);  // 10 - wrong! 0 is falsy but valid
console.log(count ?? 10);  // 0 - correct! 0 is not null/undefined

const name = "";
console.log(name || "Anonymous");  // "Anonymous" - might be wrong
console.log(name ?? "Anonymous");  // "" - preserves the empty string

// Practical use
function createUser(options = {}) {
  return {
    name: options.name ?? "Guest",
    age:  options.age  ?? 0,
    active: options.active ?? true
  };
}

createUser({ name: "", age: 0, active: false });
// { name: "", age: 0, active: false } - all falsy values preserved

What are WeakMap and WeakSet?

WeakMap and WeakSet are like Map and Set, but they hold weak references to their keys/values. If the object is garbage collected, the entry is automatically removed.

// WeakMap: keys must be objects, values can be anything
const cache = new WeakMap();

function processUser(user) {
  if (cache.has(user)) return cache.get(user);
  const result = expensiveComputation(user);
  cache.set(user, result);
  return result;
}

// When user is garbage collected, cache entry disappears automatically
// No memory leak risk

// WeakSet: stores objects, no duplicates, weakly held
const visited = new WeakSet();

function trackVisit(page) {
  if (visited.has(page)) return; // already visited
  visited.add(page);
  // process...
}

// Key differences from Map/Set:
// - Keys/values must be objects (not primitives)
// - Not iterable (no .forEach, no size property)
// - No .clear() method
// - Garbage collection happens automatically

Use WeakMap for associating private data with objects or caching without risking memory leaks.


What are Proxy and Reflect?

Proxy lets you intercept and customize fundamental operations on an object (property access, assignment, function calls, etc.). Reflect provides methods that mirror those operations.

const handler = {
  get(target, prop) {
    console.log(`Getting ${prop}`);
    return Reflect.get(target, prop); // default behavior
  },
  set(target, prop, value) {
    if (typeof value !== "number") throw new TypeError("Only numbers allowed");
    console.log(`Setting ${prop} = ${value}`);
    return Reflect.set(target, prop, value);
  }
};

const scores = new Proxy({}, handler);
scores.math = 95;    // "Setting math = 95"
scores.math;         // "Getting math"
scores.name = "hi";  // TypeError: Only numbers allowed

// Real-world use: validation, reactive systems (Vue 3 uses Proxy for reactivity)
function reactive(obj) {
  return new Proxy(obj, {
    set(target, key, value) {
      const result = Reflect.set(target, key, value);
      console.log(`State updated: ${key} = ${value}`);
      // trigger re-render, notify subscribers, etc.
      return result;
    }
  });
}

Section

Advanced Concepts

Target: Senior level - senior javascript developer interview questions

What is prototypal inheritance?

JavaScript uses prototypal inheritance - objects inherit directly from other objects through a prototype chain. Every object has an internal [[Prototype]] link to another object (or null).

// The prototype chain in action
const animal = {
  breathe() { return "breathing"; }
};

const dog = Object.create(animal); // dog's prototype is animal
dog.bark = function() { return "woof"; };

const myDog = Object.create(dog); // myDog's prototype is dog
myDog.name = "Rex";

console.log(myDog.name);     // "Rex" - own property
console.log(myDog.bark());   // "woof" - from dog's prototype
console.log(myDog.breathe()); // "breathing" - from animal's prototype

// The chain: myDog → dog → animal → Object.prototype → null
console.log(Object.getPrototypeOf(myDog) === dog);    // true
console.log(Object.getPrototypeOf(dog) === animal);   // true

What is the prototype chain and Object.create?

// Object.create: creates an object with a specific prototype
const vehicleProto = {
  describe() {
    return `${this.make} ${this.model} (${this.year})`;
  }
};

const car = Object.create(vehicleProto);
car.make = "Toyota";
car.model = "Camry";
car.year = 2024;

console.log(car.describe()); // "Toyota Camry (2024)"
console.log(car.hasOwnProperty("make"));     // true
console.log(car.hasOwnProperty("describe")); // false - it's on the prototype

// Object.create(null) - creates an object with NO prototype
const pureMap = Object.create(null);
pureMap.key = "value";
// pureMap has no toString, hasOwnProperty, etc. - truly clean slate

What is the difference between class syntax and prototypes?

ES6 class syntax is syntactic sugar over prototypal inheritance. Under the hood, it's still the same prototype chain.

// Prototype-based (ES5 style)
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function() {
  return `${this.name} makes a sound.`;
};

function Dog(name, breed) {
  Animal.call(this, name); // call parent constructor
  this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() { return "Woof!"; };

// Class syntax (ES6+) - same result, cleaner code
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a sound.`;
  }
}

class Dog extends Animal {
  #secret = "private field"; // ES2022 private fields
  
  constructor(name, breed) {
    super(name); // must call super before using this
    this.breed = breed;
  }
  bark() { return "Woof!"; }
}

const d = new Dog("Rex", "Labrador");
console.log(d.speak()); // "Rex makes a sound."
console.log(d.bark());  // "Woof!"
console.log(d instanceof Dog);    // true
console.log(d instanceof Animal); // true

Key difference: classes must be called with new, functions don't enforce this. Also, class methods are non-enumerable by default.


What are generators and iterators?

A generator is a function that can pause and resume its execution, yielding values one at a time. An iterator is any object with a next() method that returns { value, done }.

// Generator function (note the *)
function* counter(start = 0) {
  while (true) {
    yield start++;
  }
}

const gen = counter(1);
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }

// Finite generator
function* range(start, end, step = 1) {
  for (let i = start; i < end; i += step) {
    yield i;
  }
}

for (const num of range(0, 10, 2)) {
  console.log(num); // 0, 2, 4, 6, 8
}

// Generators are lazy - they compute values on demand
// Perfect for infinite sequences, pagination, streaming data
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();
Array.from({ length: 8 }, () => fib.next().value);
// [0, 1, 1, 2, 3, 5, 8, 13]

What is Symbol and when do you use it?

Symbol creates a unique, immutable primitive value. No two Symbols are ever equal, even if they have the same description.

const id1 = Symbol("id");
const id2 = Symbol("id");
console.log(id1 === id2); // false - always unique

// Primary use: unique object property keys (no collisions)
const USER_ID = Symbol("userId");
const user = {
  name: "Alice",
  [USER_ID]: 12345 // Symbol as key - won't clash with other code
};

console.log(user[USER_ID]); // 12345
console.log(user.userId);   // undefined

// Symbols are hidden from most enumeration
Object.keys(user);           // ["name"] - Symbol not included
JSON.stringify(user);        // '{"name":"Alice"}' - Symbol excluded
Object.getOwnPropertySymbols(user); // [Symbol(userId)]

// Well-known Symbols: customize built-in behavior
class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next() {
        return current <= end
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      }
    };
  }
}

for (const n of new Range(1, 5)) {
  console.log(n); // 1, 2, 3, 4, 5
}

What are memory leaks in JavaScript and how do you prevent them?

A memory leak happens when memory that's no longer needed isn't released. In JavaScript, the garbage collector handles most cleanup - but certain patterns prevent it from doing its job.

// 1. Forgotten event listeners
const button = document.getElementById("btn");
function handleClick() { /* ... */ }
button.addEventListener("click", handleClick);
// Later, when button is removed from DOM:
button.removeEventListener("click", handleClick); // ← must do this

// 2. Closures holding large objects
function createLeak() {
  const bigData = new Array(1000000).fill("data");
  return function() {
    // bigData is kept alive as long as this function exists
    console.log(bigData.length);
  };
}

// 3. Global variables
function accidentalGlobal() {
  leakedVar = "oops"; // missing var/let/const - becomes global
}

// 4. Detached DOM nodes
let detachedNode;
function createDetached() {
  const div = document.createElement("div");
  detachedNode = div; // reference kept in JS even after removed from DOM
  document.body.removeChild(document.body.appendChild(div));
}

// Prevention:
// - Use WeakMap/WeakSet for object associations
// - Remove event listeners when components unmount
// - Use 'use strict' to catch accidental globals
// - Null out references you no longer need
// - Profile with Chrome DevTools Memory tab

How does garbage collection work in JavaScript?

JavaScript uses mark-and-sweep garbage collection. The engine periodically marks all reachable objects starting from "roots" (global variables, call stack), then sweeps and frees everything that wasn't marked.

// An object becomes eligible for GC when no references point to it
let obj = { name: "Alice" }; // obj is reachable
obj = null;                   // original object is now unreachable → GC eligible

// Circular references - NOT a problem for modern mark-and-sweep
function createCircle() {
  const a = {};
  const b = {};
  a.ref = b;
  b.ref = a;
  // When createCircle returns, neither a nor b is reachable from roots
  // Both are collected - mark-and-sweep handles this correctly
}

// V8's generational GC:
// - "Young generation" (Scavenger): short-lived objects, collected frequently
// - "Old generation" (Mark-Compact): long-lived objects, collected less often
// This is why creating many short-lived objects in hot paths is expensive

Modern V8 (Chrome, Node.js) uses incremental and concurrent GC to minimize pauses. You can observe GC behavior in Chrome DevTools → Performance → Memory.


Section

DOM & Browser APIs

Target: Frontend-specific

What is event delegation?

Event delegation attaches a single listener to a parent element instead of individual listeners on each child. It works because events bubble up the DOM tree.

// Without delegation: 1000 listeners for 1000 items
document.querySelectorAll(".item").forEach(item => {
  item.addEventListener("click", handleClick); // memory-intensive
});

// With delegation: 1 listener handles all items, including future ones
document.getElementById("list").addEventListener("click", (event) => {
  const item = event.target.closest(".item");
  if (!item) return; // clicked outside an item

  console.log("Clicked:", item.dataset.id);
});

// Adding new items works automatically - no new listeners needed
const newItem = document.createElement("li");
newItem.className = "item";
newItem.dataset.id = "new-1";
newItem.textContent = "New Item";
document.getElementById("list").appendChild(newItem);

Event delegation is essential for dynamic lists, virtual scrolling, and any UI where elements are frequently added/removed.


What is the difference between event bubbling and event capturing?

Events travel in three phases: capturing (root → target), target, and bubbling (target → root).

// Bubbling (default): inner → outer
document.getElementById("child").addEventListener("click", () => {
  console.log("child clicked"); // fires first
});
document.getElementById("parent").addEventListener("click", () => {
  console.log("parent clicked"); // fires second (bubbles up)
});

// Capturing: outer → inner (pass true or { capture: true })
document.getElementById("parent").addEventListener("click", () => {
  console.log("parent capturing"); // fires FIRST
}, true);
document.getElementById("child").addEventListener("click", () => {
  console.log("child clicked"); // fires second
});

// Stop propagation
document.getElementById("child").addEventListener("click", (e) => {
  e.stopPropagation(); // prevents bubbling to parent
  // e.stopImmediatePropagation() - also stops other listeners on same element
});

// Most events bubble, but some don't: focus, blur, load, scroll
// Their capturing equivalents (focusin, focusout) do bubble

What is the difference between debounce and throttle?

Both control how often a function executes during rapid events. They solve different problems.

// DEBOUNCE: waits until the user STOPS firing events
// Use case: search input, window resize handler
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

const searchInput = document.getElementById("search");
searchInput.addEventListener("input", debounce((e) => {
  console.log("Searching:", e.target.value);
  // API call only fires 300ms after user stops typing
}, 300));

// THROTTLE: fires at most once per interval, regardless of how often triggered
// Use case: scroll events, mousemove, game loop
function throttle(fn, limit) {
  let lastCall = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastCall >= limit) {
      lastCall = now;
      return fn.apply(this, args);
    }
  };
}

window.addEventListener("scroll", throttle(() => {
  console.log("Scroll position:", window.scrollY);
  // Fires at most once every 100ms, no matter how fast you scroll
}, 100));

Debounce = "wait for the dust to settle." Throttle = "fire at a steady rate."


What is requestAnimationFrame?

requestAnimationFrame (rAF) schedules a callback to run just before the browser's next repaint - typically 60 times per second (every ~16.7ms). It's the right way to do smooth animations.

// Bad: setTimeout-based animation (inconsistent, can cause jank)
function animateBad() {
  element.style.left = parseInt(element.style.left) + 1 + "px";
  setTimeout(animateBad, 16); // not synchronized with display refresh
}

// Good: rAF-based animation (synchronized with display)
let position = 0;
let animationId;

function animate(timestamp) {
  position += 2;
  element.style.transform = `translateX(${position}px)`;

  if (position < 500) {
    animationId = requestAnimationFrame(animate);
  }
}

animationId = requestAnimationFrame(animate);

// Cancel if needed
cancelAnimationFrame(animationId);

// rAF automatically pauses when the tab is hidden - saves battery
// timestamp parameter gives you precise timing for physics-based animations

What are Web Workers?

Web Workers run JavaScript in a background thread, separate from the main thread. They can't access the DOM, but they can do heavy computation without blocking the UI.

// main.js
const worker = new Worker("worker.js");

worker.postMessage({ data: largeArray }); // send data to worker

worker.onmessage = (event) => {
  console.log("Result from worker:", event.data);
  worker.terminate(); // clean up when done
};

worker.onerror = (err) => console.error("Worker error:", err);

// worker.js (runs in separate thread)
self.onmessage = (event) => {
  const { data } = event.data;
  
  // Heavy computation - doesn't block main thread
  const result = data.reduce((sum, n) => sum + n, 0);
  
  self.postMessage(result); // send result back
};

Workers communicate via postMessage and structured cloning. Use them for image processing, cryptography, parsing large JSON, or any CPU-intensive task.


What is the difference between localStorage, sessionStorage, and cookies?

All three store data in the browser, but they differ in lifetime, scope, and size.

localStorage

sessionStorage

Cookies

Capacity

~5MB

~5MB

~4KB

Expires

Never (manual clear)

Tab close

Set by expires/max-age

Scope

Same origin, all tabs

Same tab only

Domain + path

Sent to server

No

No

Yes (every request)

Accessible via JS

Yes

Yes

Yes (unless HttpOnly)

// localStorage - persists across sessions
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme"); // "dark"
localStorage.removeItem("theme");
localStorage.clear();

// sessionStorage - cleared when tab closes
sessionStorage.setItem("sessionId", "abc123");

// Cookies - sent with every HTTP request
document.cookie = "userId=42; expires=Fri, 31 Dec 2026 23:59:59 GMT; path=/; Secure; SameSite=Strict";

// Use localStorage for user preferences, theme, cached data
// Use sessionStorage for single-session state (shopping cart step, form progress)
// Use cookies for auth tokens (with HttpOnly + Secure flags)

Section

Coding Challenges

Target: All levels - these appear in technical screens at every company

Implement debounce

Problem: Write a debounce(fn, delay) function that delays invoking fn until delay milliseconds have passed since the last call.

function debounce(fn, delay) {
  let timerId;

  return function(...args) {
    // Clear any pending timer
    clearTimeout(timerId);

    // Set a new timer
    timerId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// Test it
const log = debounce((msg) => console.log(msg), 300);
log("a"); // cancelled
log("b"); // cancelled
log("c"); // "c" - only this one fires, 300ms after last call

Key points: Each call resets the timer. this context and all arguments are preserved via apply. The returned function is a closure over timerId.


Implement throttle

Problem: Write a throttle(fn, limit) function that ensures fn is called at most once per limit milliseconds.

function throttle(fn, limit) {
  let lastCall = 0;
  let timerId;

  return function(...args) {
    const now = Date.now();
    const remaining = limit - (now - lastCall);

    if (remaining <= 0) {
      // Enough time has passed - call immediately
      if (timerId) {
        clearTimeout(timerId);
        timerId = null;
      }
      lastCall = now;
      fn.apply(this, args);
    } else {
      // Schedule a trailing call
      clearTimeout(timerId);
      timerId = setTimeout(() => {
        lastCall = Date.now();
        timerId = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
}

// Test it
const log = throttle((msg) => console.log(msg, Date.now()), 1000);
log("a"); // fires immediately
log("b"); // ignored (within 1s)
log("c"); // fires ~1s after "a" (trailing call)

Flatten a nested array

Problem: Write a function that flattens a deeply nested array to any depth.

// Modern: Array.prototype.flat()
[1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4]

// Custom recursive implementation
function flatten(arr, depth = Infinity) {
  return arr.reduce((flat, item) => {
    if (Array.isArray(item) && depth > 0) {
      return flat.concat(flatten(item, depth - 1));
    }
    return flat.concat(item);
  }, []);
}

flatten([1, [2, [3, [4, [5]]]]]);         // [1, 2, 3, 4, 5]
flatten([1, [2, [3, [4, [5]]]]], 2);      // [1, 2, 3, [4, [5]]]

// Stack-based (iterative) - avoids call stack overflow for very deep arrays
function flattenIterative(arr) {
  const stack = [...arr];
  const result = [];

  while (stack.length) {
    const item = stack.pop();
    if (Array.isArray(item)) {
      stack.push(...item); // unshift to maintain order
    } else {
      result.unshift(item);
    }
  }

  return result;
}

Deep clone an object

Problem: Write a function that creates a deep copy of an object (no shared references).

// Modern: structuredClone (ES2022) - handles most cases
const original = { a: 1, b: { c: 2 }, d: [3, 4] };
const clone = structuredClone(original);
clone.b.c = 99;
console.log(original.b.c); // 2 - unaffected

// structuredClone handles: Date, RegExp, Map, Set, ArrayBuffer
// Does NOT handle: functions, DOM nodes, class instances with methods

// JSON approach - simple but lossy
const jsonClone = JSON.parse(JSON.stringify(original));
// Loses: undefined, functions, Symbol, Date (becomes string), Infinity, NaN

// Custom recursive deep clone
function deepClone(value, seen = new WeakMap()) {
  // Handle primitives and null
  if (value === null || typeof value !== "object") return value;

  // Handle circular references
  if (seen.has(value)) return seen.get(value);

  // Handle special types
  if (value instanceof Date) return new Date(value);
  if (value instanceof RegExp) return new RegExp(value);
  if (value instanceof Map) {
    const mapClone = new Map();
    seen.set(value, mapClone);
    value.forEach((v, k) => mapClone.set(deepClone(k, seen), deepClone(v, seen)));
    return mapClone;
  }

  // Handle arrays and plain objects
  const clone = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
  seen.set(value, clone);

  for (const key of [...Object.keys(value), ...Object.getOwnPropertySymbols(value)]) {
    clone[key] = deepClone(value[key], seen);
  }

  return clone;
}

Implement Promise.all

Problem: Implement Promise.all from scratch.

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError("Argument must be an array"));
    }

    if (promises.length === 0) {
      return resolve([]);
    }

    const results = new Array(promises.length);
    let resolved = 0;

    promises.forEach((promise, index) => {
      // Wrap in Promise.resolve to handle non-Promise values
      Promise.resolve(promise)
        .then((value) => {
          results[index] = value;
          resolved++;

          if (resolved === promises.length) {
            resolve(results); // all done
          }
        })
        .catch(reject); // first rejection wins
    });
  });
}

// Test
promiseAll([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]).then(console.log); // [1, 2, 3]

promiseAll([
  Promise.resolve(1),
  Promise.reject("error"),
  Promise.resolve(3)
]).catch(console.error); // "error"

Key insight: Results are stored by index (not by resolution order) to preserve the original array order.


Find duplicates in an array

Problem: Given an array, return all elements that appear more than once.

// Using a Map - O(n) time, O(n) space
function findDuplicates(arr) {
  const count = new Map();
  const duplicates = [];

  for (const item of arr) {
    count.set(item, (count.get(item) || 0) + 1);
  }

  for (const [item, freq] of count) {
    if (freq > 1) duplicates.push(item);
  }

  return duplicates;
}

findDuplicates([1, 2, 3, 2, 4, 3, 5]); // [2, 3]
findDuplicates(["a", "b", "a", "c"]);   // ["a"]

// One-liner using filter + indexOf
function findDuplicatesShort(arr) {
  return [...new Set(arr.filter((item, index) => arr.indexOf(item) !== index))];
}

// Find duplicates with their counts
function findDuplicatesWithCount(arr) {
  const count = arr.reduce((acc, item) => {
    acc[item] = (acc[item] || 0) + 1;
    return acc;
  }, {});

  return Object.entries(count)
    .filter(([, freq]) => freq > 1)
    .map(([item, freq]) => ({ item, count: freq }));
}

findDuplicatesWithCount([1, 2, 2, 3, 3, 3]);
// [{ item: "2", count: 2 }, { item: "3", count: 3 }]

💡 Want to practice these coding challenges in a real interview environment? jsinterview.in runs your code live in the browser and uses AI to evaluate your approach, time complexity explanation, and edge case handling - just like a real technical screen. [Try a coding challenge mock interview →]


Live challenge
Easy

Two Sum Indexes

Return the indexes of the two numbers whose sum equals the target.

A hash map gives you the expected linear-time solution.

0/2 tests passing

Section

JavaScript Interview Tips

  • Think out loud. Interviewers care as much about your reasoning as your answer. Narrate your thought process - "I'm thinking about using a Map here because lookups are O(1)..." - before you write a single line.

  • Start with the naive solution, then optimize. Don't freeze trying to find the perfect answer. A working O(n²) solution you can explain beats a half-finished O(n) solution every time. Then discuss how you'd improve it.

  • Know your edge cases. Empty arrays, null inputs, circular references, very large inputs - mention them proactively. It signals senior-level thinking.

  • Brush up on the event loop before any frontend role. It comes up in some form in nearly every JavaScript interview, from "why does this setTimeout fire after the Promise?" to full-on system design questions about rendering performance.

  • Practice speaking about code, not just writing it. AI mock interviews are uniquely useful here - you can practice explaining closures or the prototype chain out loud, get instant feedback, and iterate until the explanation is clean and confident.


Section

FAQ

How long should I prepare for a JavaScript interview?

It depends on your current level. For a junior role, 2–3 weeks of focused daily practice (1–2 hours) covering fundamentals, closures, async, and the DOM is realistic. For a senior role at a top company, expect 4–8 weeks - you'll need to go deep on the event loop, prototypal inheritance, performance, and system design. The biggest mistake is spending all your time reading and not enough time practicing out loud.

What level of JavaScript questions should I expect at FAANG/top tech companies?

At companies like Google, Meta, and Amazon, the bar is high. Expect questions that combine multiple concepts - "implement a debounce function that also supports cancellation and immediate invocation" rather than "what is debounce?" You'll also be expected to discuss time/space complexity, handle edge cases proactively, and explain trade-offs. Senior javascript developer interview questions at these companies often involve designing systems (e.g., "build a rate limiter") or deep-diving into engine behavior.

What is the difference between JavaScript and TypeScript in interviews?

TypeScript is increasingly expected at senior roles, but most JavaScript interview questions are language-agnostic at the concept level. If a job description mentions TypeScript, be ready to discuss generics, type narrowing, utility types (Partial, Required, Pick, Omit), and interface vs type. In a JavaScript interview, you can write TypeScript-style annotations in comments to signal type awareness even if the question doesn't require it.

How do I practice coding live during an interview?

The biggest challenge in live coding isn't knowing the answer - it's performing under pressure. Practice in an environment that mimics the interview: use a plain code editor (not your IDE with autocomplete), talk through your solution before typing, and time yourself. AI-powered mock interviews that run your code live and ask follow-up questions are the closest thing to the real experience you can get before the actual interview.

What JavaScript topics are asked most often?

Based on patterns across hundreds of real interviews: closures (nearly universal), the event loop and async/await (universal for mid+ roles), this keyword and call/apply/bind (very common), prototypal inheritance (senior roles), debounce/throttle implementation (frontend roles), and Promise.all implementation (mid to senior). Hoisting, var/let/const, and == vs === are common at junior level.

Should I memorize all these JavaScript interview questions and answers?

No - memorization is the wrong approach. Interviewers can tell immediately when someone is reciting a rehearsed answer. Instead, understand the why behind each concept deeply enough to explain it in your own words and apply it to a new problem. If you can implement debounce from scratch and explain why you made each design decision, you don't need to memorize anything.

How important is ES6+ knowledge for a JavaScript interview?

Very important. Destructuring, spread/rest, arrow functions, Promises, async/await, template literals, and modules are all standard in modern codebases. For senior roles, expect questions on optional chaining, nullish coalescing, WeakMap/WeakSet, generators, and Proxy. Interviewers at top companies may also ask about ES2022+ features like private class fields (#field) and structuredClone.

What's the best way to answer "explain the event loop" in an interview?

Start with the big picture: JavaScript is single-threaded, so it needs a mechanism to handle async work without blocking. Then walk through the components: call stack, Web APIs, microtask queue, macrotask queue. Give a concrete example with setTimeout and a Promise, and trace the exact execution order. Bonus points: mention that microtasks drain completely before any macrotask runs, and that this is why Promise callbacks always run before setTimeout callbacks, even with a 0ms delay.


Section

Useful Sources

Internal linking boost

Continue practicing beyond this guide

Use this page as your interview revision base, then move into the full practice arena for more timed coding problems and AI-guided explanations.

Open practice arena
Buy Me A Coffee