Interview Prep

Top 50 JavaScript
Interview Questions

Master the most frequently asked JS questions — with clear explanations and real code examples you can actually remember.

50 Questions Beginner to Advanced Copy-ready code snippets Asked in real interviews
Showing 50 of 50 questions
Basics

Core JavaScript Basics

1What are the key features of JavaScript?

JavaScript is a lightweight, interpreted, high-level language with these key features:

  • Dynamic typing — types are resolved at runtime.
  • First-class functions — functions are values; pass them around like any other object.
  • Prototype-based OOP — objects inherit directly from other objects via the prototype chain.
  • Event-driven & asynchronous — callbacks, Promises, and async/await enable non-blocking code.
  • Single-threaded — one call stack, but the event loop gives the illusion of concurrency.
  • Universal runtime — runs in browsers (V8, SpiderMonkey) and on the server (Node.js, Deno).
  • var — function-scoped, hoisted and initialized as undefined, can be re-declared and reassigned.
  • let — block-scoped, hoisted but stays in the Temporal Dead Zone until the declaration line, can be reassigned but not re-declared.
  • const — block-scoped, same TDZ behavior, cannot be reassigned or re-declared. The value it holds (e.g., an object) can still be mutated.
var x = 1;
{
  var x = 10; // same variable — overwrites outer x
  let y = 20; // block-scoped, invisible outside
}
console.log(x); // 10
console.log(y); // ReferenceError

Hoisting is the engine's behaviour of moving variable and function declarations to the top of their scope before code runs — only the declaration is hoisted, not the initialisation.

console.log(a); // undefined (var is declared but not yet assigned)
var a = 5;

greet(); // works — function declarations are fully hoisted
function greet() { console.log("Hello"); }

console.log(b); // ReferenceError — let stays in TDZ
let b = 10;

== is loose equality and performs type coercion before comparing. === is strict equality — it compares value and type with no coercion.

0 == false    // true  (false is coerced to 0)
0 === false   // false (different types)
"5" == 5      // true
"5" === 5     // false

Always prefer === to avoid subtle coercion bugs.

  • undefined — a variable has been declared but not yet assigned. JavaScript sets this automatically.
  • null — an intentional absence of value. The developer explicitly sets it.
let a;           // undefined
let b = null;    // intentionally empty

typeof undefined // "undefined"
typeof null      // "object"  ← legacy quirk in JS

JavaScript has 8 data types:

  • 7 primitives: string, number, bigint, boolean, undefined, null, symbol
  • 1 non-primitive: object (arrays, functions, dates, maps, etc. are all objects)

Use typeof to check a type. Note: typeof null === "object" is a known historical quirk.

Type coercion is the automatic conversion of a value from one type to another. JavaScript does this whenever you mix types in an operation.

"5" + 2      // "52"   — number coerced to string
"5" - 2      // 3      — string coerced to number
true + 1     // 2      — boolean coerced to number
[] + {}      // "[object Object]"
{} + []      // 0      (parsed differently by the engine)

The spread operator (...) expands an iterable (array, string, object) into individual elements.

// Arrays
const a = [1, 2];
const b = [...a, 3, 4]; // [1, 2, 3, 4]

// Objects (shallow clone)
const obj = { x: 1 };
const copy = { ...obj, y: 2 }; // { x: 1, y: 2 }

// Function arguments
Math.max(...[3, 1, 4, 1, 5]); // 5

Destructuring lets you unpack values from arrays or properties from objects into distinct variables.

// Array destructuring
const [a, b, ...rest] = [1, 2, 3, 4];
// a=1, b=2, rest=[3,4]

// Object destructuring
const { name, age = 18 } = { name: "Ravi" };
// name="Ravi", age=18 (default)

// In function params
function greet({ name, city = "Hyderabad" }) {
  return `${name} from ${city}`;
}

Template literals (backtick strings) support multi-line text, embedded expressions, and tagged templates.

const name = "CodeBegun";
const msg = `Hello, ${name}!`;   // "Hello, CodeBegun!"

// Multi-line without \n
const html = `
  <div>
    <p>${msg}</p>
  </div>
`;

// Expression interpolation
console.log(`2 + 2 = ${2 + 2}`); // "2 + 2 = 4"
Functions & Scope

Functions, Scope & Closures

A closure is a function that retains access to its outer (lexical) scope even after the outer function has returned.

function counter() {
  let count = 0;
  return function () {
    count++;
    return count;
  };
}
const inc = counter();
inc(); // 1
inc(); // 2  ← count persists inside the closure

Closures power module patterns, memoization, and data encapsulation.

  • Declaration — fully hoisted; can be called anywhere in its scope.
  • Expression — assigned to a variable; only the variable is hoisted (as undefined), not the function body.
greet(); // ✓ works
function greet() { return "Hello"; }

say(); // ✗ TypeError: say is not a function
var say = function () { return "Hi"; };

Arrow functions are a concise syntax introduced in ES6. Key differences from regular functions:

  • No own this — inherits this from the surrounding (lexical) scope.
  • No arguments object (use rest params instead).
  • Cannot be used as constructors (new will throw).
  • No prototype property.
const add = (a, b) => a + b;

const obj = {
  name: "CB",
  greet() {
    setTimeout(() => {
      console.log(this.name); // "CB" — arrow captures outer this
    }, 100);
  },
};

An IIFE is a function that is defined and called immediately. It creates a private scope, preventing variable leakage into the global scope.

(function () {
  const secret = "hidden";
  console.log("runs immediately");
})();

// Arrow function IIFE
(() => {
  const x = 10;
})();

console.log(typeof secret); // "undefined" — not in global scope

All three let you set this explicitly for a function:

  • call(ctx, arg1, arg2) — invokes immediately; arguments passed individually.
  • apply(ctx, [arg1, arg2]) — invokes immediately; arguments passed as an array.
  • bind(ctx) — returns a new function with this bound; does not invoke immediately.
function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}
const user = { name: "Siva" };

greet.call(user, "Hello", "!");   // "Hello, Siva!"
greet.apply(user, ["Hi", "?"]);  // "Hi, Siva?"
const hi = greet.bind(user, "Hey");
hi(".");                          // "Hey, Siva."

Currying transforms a function with multiple arguments into a sequence of functions, each taking one argument.

// Without currying
const add = (a, b) => a + b;
add(2, 3); // 5

// Curried
const curriedAdd = (a) => (b) => a + b;
const add5 = curriedAdd(5);
add5(3); // 8
add5(10); // 15

Useful for partial application and building reusable utility functions.

Generators are functions that can pause and resume execution via the yield keyword. They return an iterator object.

function* idGen() {
  let id = 1;
  while (true) {
    yield id++;
  }
}
const gen = idGen();
gen.next().value; // 1
gen.next().value; // 2
gen.next().value; // 3

Used for lazy sequences, infinite lists, and implementing async flows (like redux-saga).

Rest parameters (...args) collect all remaining function arguments into a real array. Unlike the old arguments object, rest params are a proper Array.

function sum(...nums) {
  return nums.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3, 4); // 10

// First arg named, rest captured
function first(head, ...tail) {
  return { head, tail };
}
first(1, 2, 3); // { head: 1, tail: [2, 3] }
Async

Async JavaScript & Promises

The event loop is JavaScript's concurrency model. Since JS is single-threaded, it uses the event loop to handle asynchronous tasks without blocking.

Execution order: synchronous code → microtasks (Promises, queueMicrotask) → macrotasks (setTimeout, setInterval, I/O)

console.log("1");
setTimeout(() => console.log("3"), 0);
Promise.resolve().then(() => console.log("2"));
// Output: 1  2  3

A Promise represents a value that may be available now, in the future, or never. It has three states: pending, fulfilled, rejected.

const p = new Promise((resolve, reject) => {
  setTimeout(() => resolve("done"), 1000);
});

p
  .then(val => console.log(val))   // "done"
  .catch(err => console.error(err))
  .finally(() => console.log("cleanup"));

async/await is syntactic sugar over Promises. An async function always returns a Promise; await pauses execution inside that function until the awaited Promise settles.

async function fetchUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error("Not found");
    return await res.json();
  } catch (err) {
    console.error(err);
  }
}

Promise.all() takes an array of Promises and returns a single Promise that resolves when all resolve — or rejects as soon as any one rejects.

const [user, posts] = await Promise.all([
  fetch("/api/user").then(r => r.json()),
  fetch("/api/posts").then(r => r.json()),
]);
// Both requests run in parallel — not sequentially

Use it when multiple async operations are independent and you need all of their results.

  • Promise.race() — resolves/rejects with the first settled Promise (whichever finishes first, win or lose).
  • Promise.allSettled() — waits for all Promises and returns an array of { status, value/reason } objects, never rejects.
// race — timeout pattern
const result = await Promise.race([
  fetch("/api/slow"),
  new Promise((_, rej) => setTimeout(() => rej("timeout"), 3000))
]);

// allSettled — want all outcomes even if some fail
const results = await Promise.allSettled([p1, p2, p3]);
results.forEach(r => {
  if (r.status === "fulfilled") console.log(r.value);
  else console.warn(r.reason);
});

A callback is a function passed as an argument to another function and called when an async operation completes. Callback hell happens when callbacks are deeply nested, making code hard to read and maintain.

// Callback hell (pyramid of doom)
getUser(id, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {
      // deeply nested...
    });
  });
});

// Solution: Promises or async/await
const user = await getUser(id);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);

Web Workers run JavaScript in a background thread, separate from the main UI thread — preventing heavy computation from blocking rendering.

// main.js
const worker = new Worker("worker.js");
worker.postMessage({ data: [1, 2, 3, 4, 5] });
worker.onmessage = (e) => console.log("Result:", e.data);

// worker.js
self.onmessage = (e) => {
  const result = e.data.data.reduce((a, b) => a + b, 0);
  self.postMessage(result);
};

Workers don't have access to the DOM. Communication happens via postMessage.

OOP

OOP & Prototypes

Objects inherit properties and methods from other objects via the prototype chain. Every object has an internal [[Prototype]] link.

function Animal(name) { this.name = name; }
Animal.prototype.speak = function () {
  return `${this.name} makes a sound.`;
};
const dog = new Animal("Rex");
dog.speak(); // "Rex makes a sound."
// dog.__proto__ === Animal.prototype  → true

ES6 classes are syntactic sugar over prototypal inheritance. They make OOP patterns cleaner but the underlying mechanism is still prototype-based.

class Animal {
  constructor(name) { this.name = name; }
  speak() { return `${this.name} makes a sound.`; }
}
class Dog extends Animal {
  speak() { return `${this.name} barks!`; }
}
const d = new Dog("Rex");
d.speak();              // "Rex barks!"
d instanceof Animal;    // true
  • new Fn() — creates an object with Fn.prototype as its prototype and runs the constructor function.
  • Object.create(proto) — creates a plain object with proto as its prototype; no constructor function is run.
const proto = { greet() { return `Hi, ${this.name}`; } };

const obj = Object.create(proto);
obj.name = "Siva";
obj.greet(); // "Hi, Siva"

// new keyword
function Person(name) { this.name = name; }
const p = new Person("Siva");
p.greet; // undefined — no greet on Person.prototype

Encapsulation bundles data and the methods that operate on it, and restricts direct access to internal state. In JavaScript:

  • Closures — keep variables private inside a factory function.
  • Class private fields (#field) — enforced at the engine level (ES2022+).
class BankAccount {
  #balance = 0;
  deposit(amount) { this.#balance += amount; }
  get balance() { return this.#balance; }
}
const acc = new BankAccount();
acc.deposit(500);
acc.balance;   // 500
acc.#balance;  // SyntaxError — truly private

Mixins are a pattern for adding reusable methods to a class without using classical inheritance (avoiding the fragile base-class problem).

const Serializable = (Base) => class extends Base {
  toJSON() { return JSON.stringify(this); }
  toString() { return JSON.stringify(this); }
};

const Validatable = (Base) => class extends Base {
  isValid() { return Object.keys(this).length > 0; }
};

class User extends Serializable(Validatable(class {})) {
  constructor(name) { super(); this.name = name; }
}
new User("Siva").toJSON(); // '{"name":"Siva"}'
DOM & Events

DOM & Browser Events

The Document Object Model (DOM) is a tree-shaped, in-memory representation of an HTML/XML document that JavaScript can read and modify. Each HTML element becomes a node in the tree.

Key APIs: document.querySelector(), element.addEventListener(), element.innerHTML, element.setAttribute(), document.createElement().

Event delegation attaches a single listener to a parent element and uses event bubbling to handle events from multiple children — even dynamically added ones.

document.getElementById("list").addEventListener("click", (e) => {
  if (e.target.matches("li")) {
    console.log("Clicked:", e.target.textContent);
  }
});
// Works for <li> items added after this code runs

Benefits: fewer listeners, lower memory use, handles dynamic content.

  • innerHTML — gets/sets the HTML markup inside an element. Parses HTML tags. Risk of XSS if used with user input.
  • textContent — gets/sets raw text only. Tags are treated as literal characters. Safe for untrusted input.
const el = document.getElementById("box");
el.innerHTML   = "<b>Bold</b>";   // renders bold text
el.textContent = "<b>Bold</b>";   // shows literal string

When an event fires, it travels through the DOM in three phases:

  1. Capture phase — event travels from window down to the target.
  2. Target phase — event reaches the target element.
  3. Bubble phase — event bubbles back up to window.
// Default: bubble phase (3rd arg false)
el.addEventListener("click", handler);

// Capture phase (3rd arg true)
el.addEventListener("click", handler, true);

// Stop propagation
el.addEventListener("click", (e) => {
  e.stopPropagation(); // prevents further bubbling
});
  • localStorage — persists until explicitly cleared. ~5 MB. No expiry. Same origin only.
  • sessionStorage — persists only for the tab session (cleared on tab close). ~5 MB.
  • Cookies — sent to server with every HTTP request. Can be set to expire. Supports HttpOnly (no JS access) and Secure flags. ~4 KB limit.

Use localStorage for user preferences, sessionStorage for tab-specific state, cookies for session tokens.

  • offsetHeight — height including padding, border, and scrollbar (if any).
  • clientHeight — height including padding but excluding border and scrollbar.
  • scrollHeight — total scrollable height including hidden overflow content.
const el = document.getElementById("box");
console.log(el.offsetHeight);  // e.g. 220
console.log(el.clientHeight);  // e.g. 200
console.log(el.scrollHeight); // e.g. 500 if content overflows

MutationObserver watches for changes to a DOM tree (added/removed nodes, attribute changes, text changes) and fires a callback asynchronously.

const observer = new MutationObserver((mutations) => {
  mutations.forEach((m) => console.log(m.type, m.target));
});
observer.observe(document.body, {
  childList: true,    // watch child nodes
  subtree: true,      // and all descendants
  attributes: true,   // watch attribute changes
});
// Later:
observer.disconnect();

The Shadow DOM is a browser API that encapsulates a DOM subtree inside a host element. Styles and scripts inside a shadow root don't leak out, and external styles don't leak in — enabling true component isolation (used by Web Components).

const host = document.getElementById("my-widget");
const shadow = host.attachShadow({ mode: "open" });
shadow.innerHTML = `
  <style>p { color: teal; }</style>
  <p>Isolated content</p>
`;
// The <p> is scoped — external CSS cannot override it
Advanced

Advanced Concepts

Memoization caches the return value of a function for given inputs so repeated calls with the same arguments skip recomputation.

function memoize(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}
const fib = memoize((n) => (n <= 1 ? n : fib(n - 1) + fib(n - 2)));
fib(40); // fast
  • Map — any key type, iterable, has .size, holds strong references (prevents GC).
  • WeakMap — object keys only, not iterable, no .size, holds weak references (key objects can be garbage collected).

Use WeakMap to attach private data to objects or to cache values that should die when the object is no longer reachable.

  • Debouncing — fires only after a pause. Perfect for search inputs: don't query until the user stops typing.
  • Throttling — fires at most once per interval. Perfect for scroll/resize handlers.
// Debounce
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// Throttle
function throttle(fn, limit) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= limit) { last = now; fn(...args); }
  };
}

A Proxy wraps an object and intercepts fundamental operations (get, set, delete, etc.) via traps. Used for validation, logging, reactivity systems (Vue 3).

const handler = {
  get(target, key) {
    return key in target ? target[key] : `Key "${key}" not found`;
  },
  set(target, key, value) {
    if (typeof value !== "number") throw new TypeError("Numbers only");
    target[key] = value;
    return true;
  },
};
const obj = new Proxy({}, handler);
obj.score = 95;
obj.score;   // 95
obj.missing; // 'Key "missing" not found'

Symbol creates a guaranteed-unique value — useful as object property keys that won't collide with other keys, and for implementing well-known protocols (iterators, toPrimitive).

const id = Symbol("id");
const user = { [id]: 42, name: "Siva" };
user[id];   // 42
user.id;    // undefined — Symbol key is private-ish

// Well-known symbols
class MyList {
  [Symbol.iterator]() {
    let i = 0, data = [1, 2, 3];
    return { next: () => ({ value: data[i++], done: i > data.length }) };
  }
}
[...new MyList()]; // [1, 2, 3]
  • CommonJS (require/module.exports) — synchronous, dynamic (can require at runtime), used by Node.js historically.
  • ES Modules (import/export) — static (imports analysed at parse time), async-friendly, tree-shakeable, the standard for browsers and modern Node.
// CommonJS
const fs = require("fs");
module.exports = { readFile: fs.readFileSync };

// ES Module
import { readFileSync } from "fs";
export const readFile = readFileSync;
  • Shallow copy — copies the top-level properties. Nested objects/arrays still share the same reference.
  • Deep copy — recursively copies every nested value; fully independent clone.
const obj = { a: 1, b: { c: 2 } };

// Shallow copy — b is still shared
const shallow = { ...obj };
shallow.b.c = 99;
obj.b.c; // 99 (mutated!)

// Deep copy
const deep = JSON.parse(JSON.stringify(obj)); // simple but lossy
const deep2 = structuredClone(obj);           // modern, preferred

A service worker is a script that the browser runs in a background thread, separate from the page. It intercepts network requests, enabling:

  • Offline support — serve cached assets when there's no network.
  • Background sync — retry failed requests later.
  • Push notifications — receive messages when the app is closed.
// Register
navigator.serviceWorker.register("/sw.js");

// sw.js — intercept fetch
self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request)
      .then(cached => cached || fetch(event.request))
  );
});

Tree shaking is a dead-code elimination technique used by bundlers (Webpack, Rollup, Vite). It statically analyses ES Module import/export statements and removes exports that are never imported, reducing bundle size.

// math.js
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b; // never imported

// app.js
import { add } from "./math.js";
// After tree shaking, multiply is stripped from the bundle

Tree shaking only works with static ES Module syntax — not require().

An iterable is any object with a [Symbol.iterator]() method that returns an iterator. An iterator is an object with a next() method returning { value, done }.

// Custom iterable
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 };
      }
    };
  }
};
[...range]; // [1, 2, 3, 4, 5]

The TDZ is the period between entering a block scope where a let or const variable is declared, and the line where it is actually declared. Accessing the variable in this period throws a ReferenceError.

{
  // TDZ starts for x
  console.log(x); // ReferenceError: Cannot access 'x' before initialization
  let x = 10;     // TDZ ends here
  console.log(x); // 10
}

The TDZ exists to catch bugs that var's silent undefined hoisting would hide.

Lazy loading defers loading of a resource until it is actually needed, improving initial page performance.

  • Images — use loading="lazy" attribute (native HTML).
  • Code splitting — use dynamic import() to split JS bundles.
  • Intersection Observer — load content only when it enters the viewport.
// Dynamic import (code splitting)
const btn = document.getElementById("load");
btn.addEventListener("click", async () => {
  const { Chart } = await import("./Chart.js");
  new Chart(document.getElementById("canvas"));
});

// Intersection Observer lazy image
const io = new IntersectionObserver((entries) => {
  entries.forEach(e => {
    if (e.isIntersecting) { e.target.src = e.target.dataset.src; }
  });
});

Ready to crack your Java interviews too?

The 145-day Java Full Stack program includes mock interviews, live projects & placement support.

Apply Now →
Chat with us