Skip to main content
JavaScript intermediate Lesson 24 of 24

JavaScript Interview Preparation: Top 35 Questions

35 essential JavaScript interview questions with detailed answers and code examples — closures, hoisting, event loop, prototypes, async/await, and more.

This guide covers 35 questions that appear consistently across JavaScript interviews — from junior to senior level. Each answer is complete but concise, focused on what interviewers actually want to hear.


Fundamentals

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

varletconst
ScopeFunctionBlockBlock
HoistedYes (undefined)Yes (TDZ)Yes (TDZ)
Re-declarableYesNoNo
Re-assignableYesYesNo
function example() {
  console.log(x); // undefined (var hoisted)
  // console.log(y); // ReferenceError (TDZ)

  var x = 1;
  let y = 2;
  const z = 3;

  if (true) {
    var x = 10;   // same x — function scoped
    let y = 20;   // different y — block scoped
    console.log(y); // 20
  }
  console.log(x); // 10
  console.log(y); // 2
}

const prevents reassignment of the binding, not mutation of the value — const arr = [] still allows arr.push(1).


Q2. What is hoisting?

Hoisting is JavaScript’s behavior of moving declarations to the top of their scope during the compilation phase — before code executes.

// What you write:
console.log(name); // undefined
var name = "Alice";
sayHello();        // works!

function sayHello() {
  console.log("Hello");
}

// What the engine sees:
var name;          // declaration hoisted, initialized to undefined
function sayHello() { console.log("Hello"); } // entire function hoisted

console.log(name);
name = "Alice";
sayHello();

Function declarations are fully hoisted (body included). var is hoisted but initialized to undefined. let/const are hoisted but sit in the Temporal Dead Zone (TDZ) — accessing them before declaration throws a ReferenceError.


Q3. What is a closure?

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

function makeCounter(start = 0) {
  let count = start; // captured in closure

  return {
    increment() { return ++count; },
    decrement() { return --count; },
    value()     { return count; },
    reset()     { count = start; },
  };
}

const counter = makeCounter(10);
counter.increment(); // 11
counter.increment(); // 12
counter.decrement(); // 11
counter.value();     // 11

// Classic interview gotcha: closures in loops
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3 3 3 — all share the same `i`
}

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 0 1 2 — let creates a new binding per iteration
}

Q4. Explain == vs ===

=== (strict equality) compares type and value. == (loose equality) coerces types first.

0 == false     // true  (false coerces to 0)
"" == false    // true  (both coerce to 0)
null == undefined // true  (spec special case)
null == 0      // false (null only == undefined)
NaN == NaN     // false (NaN is never equal to itself)

0 === false    // false (different types)
"" === false   // false
null === undefined // false

// Always use ===. The only valid use of == is: x == null
// which checks for both null and undefined in one expression
function isNullOrUndefined(x) {
  return x == null; // true for null and undefined, false for everything else
}

Q5. What are the falsy values in JavaScript?

JavaScript has a fixed, small set of values that evaluate to false in a boolean context. Knowing this list precisely matters because any value not on it — including empty arrays and empty objects — is truthy, which surprises many developers. Interviewers often probe this with output questions involving conditionals.

// Exactly 8 falsy values:
false, 0, -0, 0n, "", '', ``, null, undefined, NaN

// Everything else is truthy, including:
"0"        // truthy
[]         // truthy (empty array)
{}         // truthy (empty object)
function(){} // truthy

Q6. Explain type coercion

Type coercion is JavaScript’s automatic conversion of values from one type to another when an operator or comparison requires it. It happens implicitly — unlike explicit casting — and follows rules that are often unintuitive. The + operator is especially tricky because it doubles as string concatenation, so the direction of coercion depends on the operand types. Understanding these rules helps you predict output questions and write safer comparisons.

// String concatenation vs addition
1 + "2"        // "12"  (number coerced to string)
1 + 2 + "3"   // "33"  (left-to-right: 3 then "33")
"3" + 1 + 2   // "312"

// Comparison
"5" > 3        // true  (string coerced to number)
"5" > "30"     // true  (lexicographic: "5" > "3")

// Arithmetic
"6" / "2"      // 3     (both coerced to number)
true + true    // 2
[] + []        // ""
[] + {}        // "[object Object]"
{} + []        // 0 (in some contexts {} parsed as empty block)

Functions and Scope

Q7. What is the difference between call, apply, and bind?

All three set the this value. The difference is in how arguments are passed and when the function is called.

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

const user = { name: "Alice" };

greet.call(user, "Hello", "!");         // "Hello, Alice!" — args as list
greet.apply(user, ["Hello", "!"]);     // "Hello, Alice!" — args as array
const boundGreet = greet.bind(user);   // returns a new function, not called yet
boundGreet("Hi", "?");                 // "Hi, Alice?"

// bind with partial application
const sayHelloTo = greet.bind(null, "Hello", "!");
sayHelloTo.call(user); // "Hello, Alice!" — 'this' set by call, args from bind

Q8. Explain the this keyword

this refers to the calling context at the time of invocation — not where the function is defined.

// Method call: this = the object
const obj = {
  name: "Alice",
  greet() { return this.name; },
};
obj.greet(); // "Alice"

// Detached: this = undefined (strict) or global (sloppy)
const fn = obj.greet;
fn(); // undefined in strict mode

// Arrow functions: this is lexically inherited from enclosing scope
class Timer {
  constructor() { this.ticks = 0; }

  start() {
    setInterval(() => {
      this.ticks++; // 'this' is the Timer instance
    }, 1000);
  }
}

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

Q9. What is currying?

Currying transforms a function that takes multiple arguments into a series of functions each taking one argument.

// Manual curry
function add(a) {
  return function (b) {
    return a + b;
  };
}
const add5 = add(5);
add5(3); // 8

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

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

Q10. What is memoization?

Memoization caches the result of a function call keyed by its arguments, avoiding redundant computation.

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 fib = memoize(function (n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
});

fib(40); // fast — each value computed once

Prototypes and OOP

Q11. How does prototypal inheritance work?

Every object has an internal [[Prototype]] link. When you access a property, JavaScript walks up the prototype chain until it finds it or reaches null.

const animal = {
  speak() {
    return `${this.name} makes a sound`;
  },
};

const dog = Object.create(animal);
dog.name = "Rex";
dog.bark = function () { return "Woof!"; };

dog.speak(); // "Rex makes a sound" — found on animal via chain
dog.bark();  // "Woof!" — found on dog itself

Object.getPrototypeOf(dog) === animal; // true

// ES6 class syntax compiles to this same prototype mechanism
class Animal {
  constructor(name) { this.name = name; }
  speak() { return `${this.name} speaks`; }
}

class Dog extends Animal {
  bark() { return "Woof!"; }
}

const rex = new Dog("Rex");
rex.speak(); // "Rex speaks" — inherited from Animal.prototype

Q12. What is the difference between Object.create, new, and class?

All three create objects with prototype links, but they differ in how much ceremony is involved and what runs during creation. Object.create gives you the most direct control — you supply the prototype yourself and no constructor runs. new calls a constructor function and wires up Constructor.prototype automatically. class is syntactic sugar over new that provides cleaner syntax, private fields, and better tooling support. Under the hood, class still produces the same prototype chain.

// Object.create: direct prototype link, no constructor call
const proto = { greet() { return `Hi, ${this.name}`; } };
const obj = Object.create(proto);
obj.name = "Alice";

// new: runs the constructor, sets prototype to Constructor.prototype
function User(name) { this.name = name; }
User.prototype.greet = function () { return `Hi, ${this.name}`; };
const u = new User("Alice");

// class: syntactic sugar over the above, with cleaner syntax
class User2 {
  constructor(name) { this.name = name; }
  greet() { return `Hi, ${this.name}`; }
}

Async JavaScript

Q13. Explain the Event Loop

JavaScript is single-threaded. The event loop manages the execution order of synchronous code, microtasks, and macrotasks.

Call Stack → Microtask Queue → Macrotask Queue

Microtasks: Promise callbacks, queueMicrotask, MutationObserver
Macrotasks: setTimeout, setInterval, I/O callbacks, requestAnimationFrame
console.log("1");

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

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

console.log("4");

// Output: 1, 4, 3, 2
// 1 and 4: synchronous
// 3: microtask — runs before next macrotask
// 2: macrotask — runs after all microtasks are drained

Q14. What is a Promise? Explain the states.

A Promise represents a value that will be available in the future. It has three states:

  • Pending: initial state
  • Fulfilled: operation succeeded, value available
  • Rejected: operation failed, reason available

Once settled (fulfilled or rejected), a Promise never changes state.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    Math.random() > 0.5 ? resolve("success") : reject(new Error("fail"));
  }, 1000);
});

promise
  .then((value) => console.log("Fulfilled:", value))
  .catch((err) => console.error("Rejected:", err.message))
  .finally(() => console.log("Always runs"));

// Promise.all — wait for all, fail fast on any rejection
const [users, posts] = await Promise.all([
  fetch("/api/users").then((r) => r.json()),
  fetch("/api/posts").then((r) => r.json()),
]);

// Promise.allSettled — wait for all, never rejects
const results = await Promise.allSettled([p1, p2, p3]);
results.forEach((r) => {
  if (r.status === "fulfilled") use(r.value);
  else logError(r.reason);
});

// Promise.race — first to settle wins
const result = await Promise.race([fetchData(), timeout(5000)]);

Q15. async/await vs Promises — what’s the difference?

async/await is syntactic sugar over Promises. They’re interchangeable at the runtime level.

// Promise chain
function loadUser(id) {
  return fetch(`/api/users/${id}`)
    .then((res) => {
      if (!res.ok) throw new Error("Not found");
      return res.json();
    })
    .then((user) => {
      return fetch(`/api/posts?userId=${user.id}`).then((r) => r.json());
    })
    .then((posts) => ({ user, posts }));
}

// async/await — same logic, easier to read and debug
async function loadUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error("Not found");
  const user = await res.json();
  const posts = await fetch(`/api/posts?userId=${user.id}`).then((r) => r.json());
  return { user, posts };
}

// Common mistake: sequential awaits when parallel is possible
// Slow: 2 sequential requests
const user = await fetchUser(id);
const settings = await fetchSettings(id);

// Fast: 2 parallel requests
const [user, settings] = await Promise.all([fetchUser(id), fetchSettings(id)]);

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

These four combinators give you different strategies for coordinating multiple Promises. The right choice depends on whether you need all results, can tolerate partial failures, or just want the fastest response. Promise.all is the most common — use it when every result is required and any failure should abort the whole operation. Promise.allSettled is the safe alternative when you want to process each outcome individually regardless of success or failure.

MethodResolves whenRejects when
Promise.allAll fulfillAny rejects
Promise.allSettledAll settle (never rejects)Never
Promise.raceFirst settles (either way)First rejects (if first)
Promise.anyFirst fulfillsAll reject

Advanced Concepts

Q17. What are generators?

Generators are functions that can pause and resume execution. They return an iterator.

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

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

// Infinite sequence
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();
fib.next().value; // 0
fib.next().value; // 1
fib.next().value; // 1
fib.next().value; // 2

Q18. What is a WeakMap and when would you use it?

WeakMap holds key-value pairs where keys are objects held weakly — if the object is garbage collected, the entry is automatically removed. Keys cannot be iterated.

// Use case: private data per object instance
const _private = new WeakMap();

class User {
  constructor(name, password) {
    _private.set(this, { password });
    this.name = name;
  }

  checkPassword(input) {
    return _private.get(this).password === input;
  }
}

// Use case: caching per DOM node — no memory leak when node is removed
const cache = new WeakMap();

function getComputedData(element) {
  if (cache.has(element)) return cache.get(element);
  const data = expensiveComputation(element);
  cache.set(element, data);
  return data;
}
// When element is removed from DOM and GC'd, cache entry disappears automatically

Q19. What is a Proxy?

Proxy intercepts and customizes operations on an object — reads, writes, function calls, property enumeration, and more.

// Validation proxy
function createValidator(target, schema) {
  return new Proxy(target, {
    set(obj, prop, value) {
      if (prop in schema) {
        const { type, min, max } = schema[prop];
        if (typeof value !== type) throw new TypeError(`${prop} must be ${type}`);
        if (min !== undefined && value < min) throw new RangeError(`${prop} < ${min}`);
        if (max !== undefined && value > max) throw new RangeError(`${prop} > ${max}`);
      }
      obj[prop] = value;
      return true;
    },
  });
}

const user = createValidator({}, {
  age: { type: "number", min: 0, max: 150 },
  name: { type: "string" },
});

user.name = "Alice"; // ok
user.age = 25;       // ok
user.age = -1;       // RangeError: age < 0
user.age = "old";    // TypeError: age must be number

Q20. Deep clone — how do you do it correctly?

// Bad: only shallow copy
const copy = { ...obj };
const copy2 = Object.assign({}, obj);

// Ok for JSON-safe data (no Date, undefined, functions, circular refs)
const deep = JSON.parse(JSON.stringify(obj));

// Modern: structuredClone (Node 17+, all modern browsers)
const deep2 = structuredClone(obj); // handles Date, Map, Set, circular refs

// Manual recursive clone (for learning / custom needs)
function deepClone(value, seen = new WeakMap()) {
  if (value === null || typeof value !== "object") return value;
  if (seen.has(value)) return seen.get(value); // handle circular refs
  if (value instanceof Date) return new Date(value);
  if (value instanceof RegExp) return new RegExp(value);
  if (Array.isArray(value)) {
    const clone = [];
    seen.set(value, clone);
    value.forEach((item, i) => { clone[i] = deepClone(item, seen); });
    return clone;
  }
  const clone = Object.create(Object.getPrototypeOf(value));
  seen.set(value, clone);
  for (const key of Reflect.ownKeys(value)) {
    clone[key] = deepClone(value[key], seen);
  }
  return clone;
}

Q21. What is debounce? Implement it.

Debounce is a technique that delays invoking a function until after a specified period of inactivity. Every time the debounced function is called, the timer resets — so the underlying function only runs once the caller stops firing. This is essential for performance-sensitive event handlers like search inputs or window resize listeners, where you want to react to the final state rather than every intermediate change. Its counterpart, throttle, limits calls to at most once per interval regardless of inactivity.

function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// Usage
const onSearch = debounce((query) => fetchResults(query), 300);
input.addEventListener("input", (e) => onSearch(e.target.value));

Q22. What is the difference between null and undefined?

Both represent the absence of a value, but they signal different things. undefined is what JavaScript assigns automatically — to uninitialized variables, missing function arguments, and absent object properties. null is an intentional assignment by the developer to explicitly indicate “no value here.” The distinction matters in APIs: a missing field returns undefined, while a field deliberately cleared is often set to null. The typeof null === "object" quirk is a long-standing bug in the language that was never fixed for backward compatibility reasons.

// undefined: variable declared but not assigned; missing function argument;
//            missing object property; function with no return
let x;
console.log(x);          // undefined
console.log({}.foo);     // undefined

// null: intentional absence of a value — you set it explicitly
let user = null; // no user yet

typeof undefined; // "undefined"
typeof null;      // "object" — historical bug in JS, never fixed

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

Q23. What is the Temporal Dead Zone (TDZ)?

The TDZ is the period between entering a block scope and the declaration of a let/const variable. Accessing the variable in this window throws a ReferenceError.

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

Q24. Explain the prototype chain and Object.prototype

Every plain object in JavaScript has Object.prototype at the top of its chain, which is where built-in methods like toString, hasOwnProperty, and valueOf come from. Understanding the difference between in and hasOwnProperty is a common interview point — in walks the entire chain while hasOwnProperty checks only the object itself. The chain always terminates at null, which is the prototype of Object.prototype.

const obj = { a: 1 };
// obj → Object.prototype → null

Object.getPrototypeOf(obj) === Object.prototype; // true
Object.getPrototypeOf(Object.prototype);         // null

// hasOwnProperty checks the object itself, not the chain
obj.hasOwnProperty("a");        // true
obj.hasOwnProperty("toString"); // false — toString is on Object.prototype
"toString" in obj;              // true — in checks the chain

Q25. What is event delegation?

Rather than attaching a listener to every child, attach one to a parent. Events bubble up — catch them at the parent and check the target.

// Bad: listener on every item
document.querySelectorAll(".item").forEach((el) => {
  el.addEventListener("click", handleItemClick);
});

// Good: one listener on the parent
document.getElementById("item-list").addEventListener("click", (e) => {
  const item = e.target.closest(".item");
  if (!item) return; // click was outside an item
  handleItemClick(item);
});
// Works for dynamically added items too

Output Questions

Q26. What does this output?

The typeof operator returns a string describing a value’s type, but it has several well-known quirks. typeof null returns "object" due to a historical bug, typeof NaN returns "number" even though NaN means “Not a Number”, and both arrays and plain objects return "object". Knowing these edge cases by heart is a reliable way to impress interviewers on output-prediction questions.

console.log(typeof null);        // "object"
console.log(typeof undefined);   // "undefined"
console.log(typeof NaN);         // "number"
console.log(typeof function(){}); // "function"
console.log(typeof []);          // "object"
console.log(typeof {});          // "object"

Q27. What does this output?

JavaScript passes objects by reference and primitives by value. When you assign an object to a new variable, both variables point to the same object in memory — mutating through one is visible through the other. Primitives behave oppositely: assigning a primitive to a new variable creates an independent copy.

let b = a;
b.x = 2;
console.log(a.x); // 2 — a and b point to the same object

let c = 5;
let d = c;
d = 10;
console.log(c); // 5 — primitives are copied by value

Q28. What does this output?

This question tests this binding in three different scenarios on the same object. A regular method called on the object gets the object as this. An arrow function defined in an object literal does not get the object as this — it inherits this from the surrounding scope (usually the global/module scope). Extracting a regular method into a standalone variable loses the object binding entirely, leaving this as undefined in strict mode.

const obj = {
  value: 42,
  getValue: function () { return this.value; },
  getValueArrow: () => this.value,
};

console.log(obj.getValue());       // 42
console.log(obj.getValueArrow());  // undefined (arrow's `this` is outer scope — window/global)

const fn = obj.getValue;
console.log(fn()); // undefined (lost object context)

Q29. What does this output?

This is one of the most classic JavaScript interview questions. It combines the scoping behavior of var versus let with closures inside a loop. Because var is function-scoped, all three callbacks share a single i variable that has already reached 3 by the time any of them execute. let creates a fresh binding for each iteration, so each callback closes over its own distinct copy of i.

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 3 3 3 — var is function-scoped; i is 3 by the time callbacks run

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 0 1 2 — let creates a new binding per iteration

Q30. What does this output?

This question is a direct test of event loop knowledge. Synchronous statements run first in order, then all microtasks (Promise callbacks) drain before any macrotask (setTimeout callback) runs — even a setTimeout with a delay of 0. The output order proves that the microtask queue takes priority over the macrotask queue at the end of every task.

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

Practical Implementation

Q31. Implement Function.prototype.bind

Implementing bind from scratch demonstrates that you understand how this binding, closures, and argument pre-filling work together. The native bind returns a new function with a permanently fixed this and optionally pre-applied leading arguments (partial application). The key detail is that the bound function must forward any additional arguments passed at call time after the pre-bound ones.

Function.prototype.myBind = function (thisArg, ...boundArgs) {
  const fn = this;
  return function (...callArgs) {
    return fn.apply(thisArg, [...boundArgs, ...callArgs]);
  };
};

Q32. Implement a simple EventEmitter

An EventEmitter is the backbone of the observer/pub-sub pattern — it lets components communicate without direct coupling. The core API is three methods: on to register a listener, off to remove it, and emit to invoke all listeners for an event. A clean implementation returns an unsubscribe function from on, which is more ergonomic than requiring callers to keep a reference to the original function just to call off.

class EventEmitter {
  constructor() { this.listeners = {}; }

  on(event, fn) {
    (this.listeners[event] ??= []).push(fn);
    return () => this.off(event, fn);
  }

  off(event, fn) {
    this.listeners[event] = (this.listeners[event] ?? []).filter((l) => l !== fn);
  }

  emit(event, ...args) {
    (this.listeners[event] ?? []).forEach((fn) => fn(...args));
  }
}

Q33. Flatten a nested array without Array.flat

This question tests recursion and array manipulation fundamentals. The recursive approach using reduce is idiomatic and concise — for each item, either recurse into it if it’s an array, or append it directly to the accumulator. The iterative version using a stack avoids call-stack depth limits for very deeply nested arrays, making it safer for production use on unknown input.

function flatten(arr) {
  return arr.reduce((acc, item) =>
    Array.isArray(item) ? acc.concat(flatten(item)) : acc.concat(item),
    []
  );
}

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

// Iterative version
function flattenIterative(arr) {
  const stack = [...arr];
  const result = [];
  while (stack.length) {
    const item = stack.pop();
    if (Array.isArray(item)) stack.push(...item);
    else result.unshift(item);
  }
  return result;
}

Q34. Implement pipe / compose

Pipe and compose are functional programming utilities for chaining transformations. Both take a list of functions and return a single function that passes a value through each one in sequence. The only difference is direction: pipe processes left-to-right (matching reading order), while compose processes right-to-left (matching mathematical function notation). Both are implemented in a single line using reduce or reduceRight.

// pipe: left-to-right composition
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);

// compose: right-to-left composition
const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x);

const transform = pipe(
  (x) => x * 2,
  (x) => x + 1,
  (x) => x ** 2
);

transform(3); // ((3*2)+1)^2 = 49

Q35. What is the difference between shallow and deep equality?

Shallow equality compares object references — two distinct objects with identical contents are not shallowly equal. Deep equality recursively compares all nested values, regardless of whether they share a reference. JavaScript has no built-in deep equality operator, so you either write one, use JSON.stringify (unreliable for non-JSON values like Date or undefined), or reach for a library like Lodash’s isEqual. The recursive implementation below handles nested objects and arrays but not special types like Date, Map, or RegExp.

// Shallow: checks top-level references only
const a = { x: { y: 1 } };
const b = { x: { y: 1 } };

a === b;                   // false — different references
a.x === b.x;              // false — different nested references
JSON.stringify(a) === JSON.stringify(b); // true — but unreliable for non-JSON values

// Deep equality implementation
function deepEqual(a, b) {
  if (a === b) return true;
  if (a === null || b === null) return false;
  if (typeof a !== typeof b) return false;
  if (typeof a !== "object") return false;
  if (Array.isArray(a) !== Array.isArray(b)) return false;

  const keysA = Object.keys(a);
  const keysB = Object.keys(b);
  if (keysA.length !== keysB.length) return false;

  return keysA.every((key) => deepEqual(a[key], b[key]));
}

deepEqual({ x: { y: 1 } }, { x: { y: 1 } }); // true

Quick Reference

TopicKey Point
var vs let/constvar: function scope, hoisted. let/const: block scope, TDZ
HoistingDeclarations move up; var → undefined, let/const → TDZ
ClosureFunction retains access to outer scope after outer returns
thisSet at call time; arrow functions inherit lexically
Prototype chainProperty lookup walks up [[Prototype]] until null
Event loopSync → microtasks (Promises) → macrotasks (setTimeout)
== vs ===== coerces types; always use ===
Debounce vs throttleDebounce: fires after quiet. Throttle: fires at most every N ms
Promise statesPending → Fulfilled or Rejected (irreversible)
WeakMapObject-keyed, weakly held, not iterable, GC-friendly

Frequently Asked Questions

What topics come up most in JavaScript interviews?
Closures, the event loop, prototypal inheritance, and async/await dominate. Interviewers also frequently ask about this binding, var/let/const scoping, type coercion, and output-prediction questions. Understanding these deeply covers roughly 80% of what you'll encounter.
How should I practice for a JavaScript interview?
Read each concept, then close the page and write the code from scratch. For output questions, trace through manually before running. Study the spec behavior, not just what works in practice — interviewers often probe edge cases where intuition fails.
Do I need to know ES2022+ features for interviews?
Know ES6–ES2020 thoroughly — Promises, async/await, destructuring, classes, modules, generators, WeakMap/WeakSet. Newer features (optional chaining, nullish coalescing, at(), structuredClone) are good to know but rarely the focus.