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,SymbolReference: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?
var | let | const | |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisting | Yes (as undefined) | Yes (TDZ) | Yes (TDZ) |
| Re-declare | Yes | No | No |
| Re-assign | Yes | Yes | No |
// 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
letorconstvariable is declared. Accessing the variable during this window throws aReferenceError.
{
// 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?
thisis determined at call time, not where the function is defined. Except arrow functions, which capturethisfrom their lexical (definition-time) scope.
| How called | this value |
|---|---|
obj.method() | obj |
fn() standalone | undefined (strict) / window (sloppy) |
new Fn() | new object |
| Arrow function | Inherited 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 hitsnull.
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.classsyntax 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
Promiserepresents the eventual completion or failure of an async operation. States:pending→fulfilledORrejected(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?
| Method | Resolves when | Rejects when |
|---|---|---|
Promise.all | All fulfill | Any rejects (fail-fast) |
Promise.allSettled | All settle (either way) | Never rejects |
Promise.race | First settles (win or lose) | First rejects |
Promise.any | First fulfills | All 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.
asyncfunctions always return a Promise.awaitpauses 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
delayms. Good for search input, resize. Throttle — runs the function at most once everylimitms. 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— liketextContentbut 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?
localStorage | sessionStorage | Cookies | |
|---|---|---|---|
| Capacity | ~5MB | ~5MB | ~4KB |
| Expiry | Never (until cleared) | Tab/session close | Configurable |
| Sent to server | No | No | Yes (every request) |
| Accessible from | JS only | JS only | JS + HTTP headers |
| Scope | Origin | Origin + tab | Domain + 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
| Question | Answer |
|---|---|
null == undefined | true |
null === undefined | false |
typeof null | "object" |
typeof NaN | "number" |
NaN === NaN | false |
[] == false | true (coercion) |
[] === false | false |
{} + [] | "[object Object]" |
[] + {} | "[object Object]" |
[] + [] | "" |
| Falsy values | false, 0, -0, 0n, "", null, undefined, NaN |
| Truthy edge cases | [], {}, "0", "false" are all truthy |