Back to Notes

JavaScript Interview Questions

Core Language

Q: What are JavaScript's data types?

8 types — 7 primitives + 1 object type. Primitives: Number, BigInt, String, Boolean, Null, Undefined, Symbol Reference: Object (includes Array, Function, Date, Map, Set, etc.)

typeof 42           // "number"
typeof "hello"      // "string"
typeof true         // "boolean"
typeof undefined    // "undefined"
typeof null         // "object"  ← famous JS bug, null is NOT an object
typeof {}           // "object"
typeof []           // "object"  — use Array.isArray() to check
typeof function(){} // "function"
typeof Symbol()     // "symbol"

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

=== (strict equality) — checks value AND type. No coercion. Always prefer this. == (loose equality) — coerces types before comparing.

0 == false    // true  (false coerced to 0)
0 === false   // false (different types)

"" == false   // true
"" === false  // false

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

NaN == NaN    // false — NaN is not equal to itself
Number.isNaN(NaN)   // true — correct way to check

Q: What is null vs undefined?

undefined — declared but never assigned a value (JS default) null — explicitly set to "no value" (intentional absence)

let x;           // undefined
let y = null;    // null — deliberate empty value

// Both are falsy:
if (!undefined)  // true
if (!null)       // true

// Strict check for either:
if (x == null)   // true for both null and undefined (loose equality)
if (x === null)  // true only for null

Q: What is type coercion? Give examples.

JS automatically converts types in certain operations. The rules are inconsistent — a common source of bugs.

// String + anything = string concatenation
"5" + 3      // "53"
"5" - 3      // 2  ← subtraction forces numeric

// Comparison coercion
"5" > 3      // true (string "5" → number 5)
null > 0     // false
null == 0    // false
null >= 0    // true  ← inconsistent!

// Falsy values: false, 0, "", null, undefined, NaN
// Everything else is truthy (including [], {}, "0")
Boolean([])   // true
Boolean({})   // true
Boolean("0")  // true

Q: What is var vs let vs const?

varletconst
ScopeFunctionBlockBlock
HoistingYes (as undefined)Yes (TDZ)Yes (TDZ)
Re-declareYesNoNo
Re-assignYesYesNo
// var — hoisted and function-scoped (leaks out of blocks)
if (true) { var x = 1; }
console.log(x);  // 1 — leaked out of block

// let/const — block-scoped
if (true) { let y = 1; }
console.log(y);  // ReferenceError

// TDZ — accessing let/const before declaration
console.log(z);  // ReferenceError
let z = 5;

// const — binding is immutable, not the value
const arr = [1, 2, 3];
arr.push(4);   // ✅ — array content can change
arr = [];      // TypeError — can't reassign the binding

Q: What is hoisting?

Variable and function declarations are moved to the top of their scope during the compilation phase. Only declarations are hoisted — not initialisations.

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

// var — hoisted as undefined
console.log(x);        // undefined (not ReferenceError)
var x = 5;

// let/const — hoisted but in TDZ, not accessible
console.log(y);        // ReferenceError
let y = 5;

// Function expressions — NOT hoisted
sayHi();               // TypeError: sayHi is not a function
var sayHi = function() { console.log("Hi"); };

Q: What is the Temporal Dead Zone (TDZ)?

The period between entering a scope and the point where a let or const variable is declared. Accessing the variable during this window throws a ReferenceError.

{
  // TDZ starts here for 'x'
  console.log(x);  // ReferenceError — x is in TDZ
  let x = 5;       // TDZ ends here
  console.log(x);  // 5
}

Scope & Closures

Q: What is a closure?

A function that retains access to its lexical scope even when executed outside of it. The inner function "closes over" the outer function's variables.

function makeCounter() {
  let count = 0;               // private variable
  return {
    increment: () => ++count,
    decrement: () => --count,
    value: () => count
  };
}

const counter = makeCounter();
counter.increment();  // 1
counter.increment();  // 2
counter.value();      // 2

Common use cases: data privacy, function factories, memoization, event handlers, partial application.


Q: What is the closure loop gotcha with var?

// Bug — all callbacks share the same `i` (var is function-scoped)
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3

// Fix 1 — use let (block-scoped, new binding per iteration)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2

// Fix 2 — IIFE to create a new scope per iteration
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 100);
  })(i);
}

Q: What is the difference between function declaration and function expression?

// Declaration — hoisted fully, available before definition
function add(a, b) { return a + b; }

// Expression — not hoisted, assigned to variable
const add = function(a, b) { return a + b; };

// Arrow function expression — also not hoisted, no own `this`
const add = (a, b) => a + b;

this Keyword

Q: How does this work in JavaScript?

this is determined at call time, not where the function is defined. Except arrow functions, which capture this from their lexical (definition-time) scope.

How calledthis value
obj.method()obj
fn() standaloneundefined (strict) / window (sloppy)
new Fn()new object
Arrow functionInherited from outer scope
.call(ctx) / .apply(ctx)ctx
.bind(ctx)ctx (permanently)
const obj = {
  name: "Alice",
  regular: function() { return this.name; },    // "Alice"
  arrow: () => this.name,                        // undefined (lexical this = window/global)
};

// Arrow function in class — good for event handlers
class Button {
  constructor() { this.label = "Click me"; }
  handleClick = () => console.log(this.label);  // this is always the instance
}

Q: What is the difference between call, apply, and bind?

function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}

const user = { name: "Alice" };

// call — invoke immediately, args individually
greet.call(user, "Hello", "!");     // "Hello, Alice!"

// apply — invoke immediately, args as array
greet.apply(user, ["Hello", "!"]); // "Hello, Alice!"

// bind — returns NEW function with this bound, call later
const boundGreet = greet.bind(user, "Hi");
boundGreet(".");  // "Hi, Alice."

Prototypes & Inheritance

Q: What is the prototype chain?

Every JS object has an internal [[Prototype]] link to another object. When you access a property that doesn't exist on an object, JS walks up the chain until it finds it or hits null.

const animal = { breathes: true };
const dog = Object.create(animal);   // dog.__proto__ === animal
dog.name = "Rex";

dog.name      // "Rex" — own property
dog.breathes  // true  — found on prototype
dog.toString  // function — found on Object.prototype

// Writing NEVER goes up the chain — always creates on the object itself
dog.breathes = false;  // creates own property on dog, doesn't modify animal

Q: What is the difference between prototypal and classical inheritance?

Classical (Java/C++) — classes are blueprints; objects are instances; inheritance via extends. Prototypal (JS) — objects inherit directly from other objects via the prototype chain. class syntax in JS is syntactic sugar over prototypal inheritance.

// ES6 class (sugar over prototypes)
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 Dog            // true
d instanceof Animal         // true
Object.getPrototypeOf(d) === Dog.prototype  // true

Async JavaScript

Q: What is the event loop?

JS is single-threaded. The event loop manages async operations by moving completed callbacks from queues to the call stack when it's empty.

Call Stack → runs synchronous code
     ↑
Microtask Queue → Promise callbacks (.then, .catch), queueMicrotask
     ↑
Macrotask Queue → setTimeout, setInterval, I/O, UI events

Microtasks always drain completely before the next macrotask runs.

console.log("1");                        // sync
setTimeout(() => console.log("2"), 0);  // macrotask
Promise.resolve().then(() => console.log("3")); // microtask
console.log("4");                        // sync

// Output: 1, 4, 3, 2

Q: What are Promises? What are their states?

A Promise represents the eventual completion or failure of an async operation. States: pendingfulfilled OR rejected (immutable once settled)

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

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

// Chain — each .then returns a new Promise
fetch(url)
  .then(res => res.json())           // return value becomes next .then's input
  .then(data => console.log(data))
  .catch(err => console.error(err));

Q: What is the difference between Promise.all, Promise.allSettled, Promise.race, and Promise.any?

MethodResolves whenRejects when
Promise.allAll fulfillAny rejects (fail-fast)
Promise.allSettledAll settle (either way)Never rejects
Promise.raceFirst settles (win or lose)First rejects
Promise.anyFirst fulfillsAll reject (AggregateError)
// Run in parallel — fastest pattern
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);

// Don't care about individual failures
const results = await Promise.allSettled([p1, p2, p3]);
results.forEach(r => {
  if (r.status === "fulfilled") console.log(r.value);
  else console.log(r.reason);
});

Q: What is async/await and how does it relate to Promises?

Syntactic sugar over Promises. async functions always return a Promise. await pauses the async function until the Promise settles.

// Sequential — each waits for previous (slow)
const user = await fetchUser(id);
const posts = await fetchPosts(user.id);

// Parallel — run simultaneously (fast)
const [user, posts] = await Promise.all([fetchUser(id), fetchPosts(id)]);

// Error handling
async function getData() {
  try {
    const data = await fetchSomething();
    return data;
  } catch (err) {
    console.error(err);
  }
}

// Avoid await in loops — use Promise.all instead
// ❌ Sequential:
for (const id of ids) { await fetch(id); }

// ✅ Parallel:
await Promise.all(ids.map(id => fetch(id)));

Functions & Patterns

Q: What is debounce vs throttle?

Debounce — runs the function only after the user has stopped triggering it for delay ms. Good for search input, resize. Throttle — runs the function at most once every limit ms. Good for scroll, mousemove.

// Debounce — "run after pause"
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// Throttle — "run at start, then wait"
function throttle(fn, limit) {
  let throttled = false;
  return function(...args) {
    if (!throttled) {
      fn.apply(this, args);
      throttled = true;
      setTimeout(() => { throttled = false; }, limit);
    }
  };
}

Q: What is currying?

Transforming a function that takes multiple arguments into a chain of functions that each take one argument.

// Non-curried
const add = (a, b) => a + b;
add(2, 3);  // 5

// Curried
const curriedAdd = a => b => a + b;
curriedAdd(2)(3);  // 5

const add5 = curriedAdd(5);  // partial application
add5(3);  // 8

// Generic curry implementation
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) return fn.apply(this, args);
    return (...more) => curried.apply(this, args.concat(more));
  };
}

Q: What is memoization?

Caching the result of a function call so repeated calls with the same arguments return the cached result instead of recomputing.

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.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const expensiveCalc = memoize((n) => {
  // some heavy computation
  return n * n;
});

expensiveCalc(5);  // computes → 25
expensiveCalc(5);  // returns cached 25

ES6+ Features

Q: What is destructuring?

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

// Object destructuring
const { name, age, city = "Unknown" } = user;  // city has default

// Rename while destructuring
const { name: userName } = user;

// In function params
function greet({ name, age }) {
  return `${name} is ${age}`;
}

Q: What are the uses of the spread operator ...?

// Copy array / object (shallow)
const arr2 = [...arr1];
const obj2 = { ...obj1 };

// Merge
const merged = { ...defaults, ...overrides };

// Spread into function args
Math.max(...[1, 2, 3]);   // 3

// Convert iterable to array
const chars = [..."hello"];  // ['h','e','l','l','o']

Q: What is optional chaining ?. and nullish coalescing ???

// Optional chaining — short-circuits if null/undefined
user?.address?.city         // undefined instead of TypeError
user?.getRole?.()           // safe method call
arr?.[0]                    // safe index access

// Nullish coalescing — fallback only for null/undefined (not 0 or "")
const name = user.name ?? "Anonymous";   // "Anonymous" only if null/undefined
const count = user.count ?? 0;           // 0 if null/undefined; keeps 0 if count=0

// vs OR operator — || treats all falsy as missing
const count = user.count || 0;  // replaces 0 with 0 — but also replaces "" with 0

Q: What are WeakMap and WeakSet?

Like Map/Set but keys must be objects and are weakly referenced — they don't prevent garbage collection. Not enumerable (no .size, no iteration). Use case: attaching private metadata to objects without preventing GC.

const cache = new WeakMap();

function process(obj) {
  if (cache.has(obj)) return cache.get(obj);
  const result = heavyComputation(obj);
  cache.set(obj, result);   // when obj is GC'd, entry auto-removed
  return result;
}

DOM & Events

Q: What is event bubbling and capturing?

Events travel in 3 phases: capture (top → target), target, bubble (target → top). By default, handlers fire during the bubble phase.

// Bubbling (default)
elem.addEventListener('click', handler);

// Capturing
elem.addEventListener('click', handler, { capture: true });

// Stop propagation — prevent event from travelling further
event.stopPropagation();

// Prevent default browser action (form submit, link follow)
event.preventDefault();

Q: What is event delegation?

Attach one listener on a parent instead of many on children. Uses bubbling. Works for dynamically added elements.

document.getElementById('list').addEventListener('click', (e) => {
  if (e.target.matches('li')) {
    console.log('Clicked:', e.target.textContent);
  }
});

Benefits: fewer listeners → less memory; handles dynamic children automatically.


Q: What is the difference between innerHTML, textContent, and innerText?

innerHTML — gets/sets raw HTML. Parses tags. XSS risk if used with user input. textContent — gets/sets plain text. Fast. Doesn't trigger layout. innerText — like textContent but respects CSS visibility and triggers layout reflow.

elem.innerHTML = "<b>Bold</b>";       // renders bold text
elem.textContent = "<b>Bold</b>";     // shows literal string <b>Bold</b>

// Safe user content — always use textContent, never innerHTML with user data
elem.textContent = userInput;         // safe from XSS

Performance & Patterns

Q: What causes layout thrashing and how do you avoid it?

Layout thrashing — alternating reads and writes to the DOM forces the browser to recalculate layout repeatedly (expensive).

// Bad — read/write interleaved (forces reflow on each iteration)
boxes.forEach(box => {
  const width = box.offsetWidth;         // read (forces layout)
  box.style.width = (width * 2) + "px"; // write
});

// Good — batch reads then batch writes
const widths = boxes.map(box => box.offsetWidth); // all reads
boxes.forEach((box, i) => {
  box.style.width = (widths[i] * 2) + "px";       // all writes
});

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

localStoragesessionStorageCookies
Capacity~5MB~5MB~4KB
ExpiryNever (until cleared)Tab/session closeConfigurable
Sent to serverNoNoYes (every request)
Accessible fromJS onlyJS onlyJS + HTTP headers
ScopeOriginOrigin + tabDomain + path
localStorage.setItem("key", JSON.stringify(value));
const val = JSON.parse(localStorage.getItem("key"));
localStorage.removeItem("key");

Common Gotchas

Q: What are common JavaScript gotchas?

// 1. typeof null is "object"
typeof null === "object"  // true — use === null

// 2. NaN is not equal to itself
NaN === NaN  // false
Number.isNaN(NaN)  // true ✅

// 3. Array.isArray vs typeof
typeof []  // "object" — use Array.isArray([])

// 4. parseInt with radix
parseInt("08")    // 8 (modern), was 0 in old engines
parseInt("08", 10)  // always specify radix 10

// 5. Floating point
0.1 + 0.2 === 0.3  // false (0.30000000000000004)
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON  // true ✅

// 6. Arguments are passed by value for primitives, by reference for objects
function mutate(obj) { obj.x = 99; }
const o = { x: 1 };
mutate(o);
o.x;  // 99 — object was mutated

// 7. delete removes object properties, not variables
const obj = { a: 1 };
delete obj.a;  // ✅ true
delete a;      // ❌ false (can't delete variable)

// 8. Comma operator — evaluates both, returns last
const x = (1, 2, 3);  // x = 3

// 9. Short-circuit evaluation
const name = user && user.name;     // undefined if user is falsy
const label = name || "Anonymous";  // "Anonymous" if name is falsy

// 10. Array holes vs undefined
const arr = [1, , 3];   // arr[1] is undefined but arr has a "hole"
1 in arr   // false  (hole)
arr[1]     // undefined

Quick Reference Cheatsheet

QuestionAnswer
null == undefinedtrue
null === undefinedfalse
typeof null"object"
typeof NaN"number"
NaN === NaNfalse
[] == falsetrue (coercion)
[] === falsefalse
{} + []"[object Object]"
[] + {}"[object Object]"
[] + []""
Falsy valuesfalse, 0, -0, 0n, "", null, undefined, NaN
Truthy edge cases[], {}, "0", "false" are all truthy