JavaScript interview questions
var vs let vs const
Scope
jsfunction example() { var a = 1; // function-scoped let b = 2; // block-scoped const c = 3; // block-scoped if (true) { var a = 10; // same variable - overwrites! let b = 20; // new variable - only inside this block console.log(b); // 20 } console.log(a); // 10 - was overwritten console.log(b); // 2 - untouched }Hoisting
jsconsole.log(a); // undefined - hoisted but not initialized console.log(b); // ReferenceError - temporal dead zone var a = 1; let b = 2;Reassignment
jsvar x = 1; x = 2; // ok let y = 1; y = 2; // ok const z = 1; z = 2; // Error - cannot reassign // const with objects - reference is locked, contents are not const user = { name: 'Alice' }; user.name = 'Bob'; // ok user = {}; // ErrorComparison
varletconstScope Function Block Block Hoisting Yes (undefined) TDZ TDZ Reassign Yes Yes No Re-declare Yes No No Rule: use
constby default,letwhen you need to reassign, never usevar.updated
did you know it?Event Loop
JavaScript is single-threaded - it can only do one thing at a time. The event loop is the mechanism that handles asynchronous operations.
Components
text┌─────────────────────────────────┐ │ Call Stack │ <- executes synchronous code ├─────────────────────────────────┤ │ Microtask Queue │ <- Promise callbacks, queueMicrotask ├─────────────────────────────────┤ │ Macrotask Queue │ <- setTimeout, setInterval, I/O └─────────────────────────────────┘Order of execution
- Run all synchronous code (call stack).
- Run all microtasks (Promise
.then,queueMicrotask). - Run one macrotask (setTimeout, setInterval).
- Repeat from step 2.
Example
jsconsole.log('1 - sync'); setTimeout(() => console.log('2 - macrotask'), 0); Promise.resolve().then(() => console.log('3 - microtask')); console.log('4 - sync'); // Output: // 1 - sync // 4 - sync // 3 - microtask // 2 - macrotaskWhy microtasks come first
jsPromise.resolve() .then(() => console.log('micro 1')) .then(() => console.log('micro 2')); setTimeout(() => console.log('macro'), 0); // micro 1 -> micro 2 -> macro // ALL microtasks drain before any macrotask runsUnderstanding the event loop is critical for debugging timing bugs, infinite loops in Promises, and understanding why
setTimeout(fn, 0)doesn't mean "run immediately".updated
did you know it?Closures
A closure is a function that remembers the variables from its outer scope even after that scope has finished executing.
Basic example
jsfunction makeCounter() { let count = 0; // this variable is "closed over" return function() { count++; return count; }; } const counter = makeCounter(); counter(); // 1 counter(); // 2 counter(); // 3 // count is private - can't be accessed from outsidePractical use cases
Private state (data encapsulation)
jsfunction createStore(initialValue) { let state = initialValue; return { get: () => state, set: (value) => { state = value; }, }; }Function factories
jsfunction multiply(factor) { return (number) => number * factor; } const double = multiply(2); const triple = multiply(3); double(5); // 10 triple(5); // 15Memoization
jsfunction memoize(fn) { const cache = new Map(); return function(key) { if (cache.has(key)) return cache.get(key); const result = fn(key); cache.set(key, result); return result; }; }Every function in JavaScript is a closure - it always has access to its surrounding scope. The interesting cases are when the outer function has already returned.
updated
did you know it?Prototypal Inheritance
In JavaScript, objects inherit from other objects through a prototype chain. Every object has an internal
[[Prototype]]link to another object.Prototype chain
jsconst animal = { speak() { return `${this.name} makes a sound`; } }; const dog = Object.create(animal); dog.name = 'Rex'; dog.speak(); // 'Rex makes a sound' // dog has no speak() - JS looks up the chain to animalHow property lookup works
textdog.speak() -> look in dog: not found -> look in animal (dog's prototype): found! -> look in Object.prototype: ... -> null - end of chainClass syntax (sugar over prototypes)
jsclass 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' // under the hood: // Dog.prototype -> Animal.prototype -> Object.prototype -> nullChecking the chain
jsd instanceof Dog; // true d instanceof Animal; // true Object.getPrototypeOf(d) === Dog.prototype; // trueJavaScript
classis syntactic sugar - under the hood it still uses prototype chains. Understanding this helps debug unexpected behavior withthisand inheritance.updated
did you know it?== vs ===
=== (strict equality)
Compares value and type - no conversion.
js1 === 1 // true 1 === '1' // false - different types null === null // true null === undefined // false== (loose equality)
Compares values after type coercion - JavaScript converts types before comparing.
js1 == '1' // true - '1' converted to 1 0 == false // true - false converted to 0 null == undefined // true - special rule null == 0 // false - special rule [] == false // true - both convert to 0 '' == false // true - both convert to 0The coercion rules are complex
js[] == ![] // true - this is why == is confusing // [] -> 0, ![] -> false -> 0Object comparison
js// both == and === compare references for objects const a = { x: 1 }; const b = { x: 1 }; a == b // false a === b // false a === a // trueAlways use
===. The loose equality rules are confusing, hard to remember, and a common source of bugs. The only exception some teams make isx == nullto check for bothnullandundefinedat once.updated
did you know it?Promises vs async/await
Both handle asynchronous operations -
async/awaitis syntactic sugar built on top of Promises.Promise
jsfunction getUser(id) { return fetch(`/api/users/${id}`) .then(res => res.json()) .then(user => { return fetch(`/api/posts/${user.id}`); }) .then(res => res.json()) .catch(err => console.error(err)); }async/await - same logic, readable code
jsasync function getUser(id) { try { const res = await fetch(`/api/users/${id}`); const user = await res.json(); const postsRes = await fetch(`/api/posts/${user.id}`); return await postsRes.json(); } catch (err) { console.error(err); } }Promise states
State Description pendingInitial state - not yet settled fulfilledOperation completed successfully rejectedOperation failed Parallel execution
js// sequential - slow (waits for each) const user = await getUser(); const posts = await getPosts(); // parallel - fast (runs simultaneously) const [user, posts] = await Promise.all([getUser(), getPosts()]);Other Promise methods
jsPromise.allSettled([...]) // waits for all, even if some reject Promise.race([...]) // resolves/rejects with the first settled Promise.any([...]) // resolves with the first fulfilledUse
async/awaitfor readability. Use raw Promises when composing withPromise.all,Promise.race, or building utilities.updated
did you know it?call, apply, bind
All three control what
thisrefers to when calling a function.call - invoke immediately with args spread
jsfunction greet(greeting, punctuation) { return `${greeting}, ${this.name}${punctuation}`; } const user = { name: 'Alice' }; greet.call(user, 'Hello', '!'); // 'Hello, Alice!'apply - invoke immediately with args as array
jsgreet.apply(user, ['Hello', '!']); // 'Hello, Alice!' // useful for spreading arrays into functions const nums = [3, 1, 4, 1, 5]; Math.max.apply(null, nums); // 5 // modern equivalent: Math.max(...nums)bind - returns a new function with this locked
jsconst greetAlice = greet.bind(user, 'Hello'); greetAlice('!'); // 'Hello, Alice!' greetAlice('?'); // 'Hello, Alice?' // common use case - preserving this in callbacks class Timer { constructor() { this.count = 0; } start() { setInterval(this.tick.bind(this), 1000); } tick() { this.count++; } }Comparison
Invokes immediately Args format Returns callYes Spread Result applyYes Array Result bindNo Spread New function In modern code
bindis often replaced by arrow functions (() => this.tick()), andapplyis replaced by the spread operator. But understanding all three is essential for interviews.updated
did you know it?Hoisting
Hoisting is JavaScript's behavior of moving declarations to the top of their scope during the compilation phase - before any code runs.
Function declarations - fully hoisted
jssayHello(); // works fine! function sayHello() { console.log('Hello'); }The entire function is hoisted - you can call it before it's defined.
var - hoisted but not initialized
jsconsole.log(name); // undefined (not an error) var name = 'Alice'; console.log(name); // 'Alice' // what JavaScript actually sees: var name; // declaration hoisted console.log(name); // undefined name = 'Alice'; // assignment stayslet and const - temporal dead zone (TDZ)
jsconsole.log(age); // ReferenceError: Cannot access before initialization let age = 25;letandconstare hoisted but not initialized - accessing them before declaration throws an error.Function expressions - not hoisted
jssayHi(); // TypeError: sayHi is not a function var sayHi = function() { console.log('Hi'); }; // var is hoisted as undefined, calling undefined() throwsSummary
Hoisted Initialized Usable before declaration functionYes Yes Yes varYes undefinedYes (but undefined) let/constYes No (TDZ) No Hoisting is one of the most common JavaScript interview topics. The key insight is that
letandconstARE hoisted - they just aren't usable until the declaration line.updated
did you know it?Shallow vs Deep Copy
The problem - objects are reference types
jsconst original = { name: 'Alice', address: { city: 'NYC' } }; const ref = original; ref.name = 'Bob'; console.log(original.name); // 'Bob' - same object!Shallow copy - copies only the top level
jsconst shallow1 = { ...original }; const shallow2 = Object.assign({}, original); shallow1.name = 'Bob'; // ok - doesn't affect original shallow1.address.city = 'LA'; // mutates original.address too! // address is still a reference to the same objectDeep copy - copies everything recursively
structuredClone (modern, recommended)
jsconst deep = structuredClone(original); deep.address.city = 'LA'; // original.address.city is still 'NYC'JSON trick (limited)
jsconst deep = JSON.parse(JSON.stringify(original)); // loses: undefined, functions, Date objects, Map, Set, circular refslodash cloneDeep
jsimport { cloneDeep } from 'lodash'; const deep = cloneDeep(original);Comparison
Method Type Handles Date/Map/Set Circular refs { ...obj }Shallow - - Object.assignShallow - - structuredCloneDeep Yes Yes JSONtrickDeep No No lodash.cloneDeepDeep Yes Yes structuredCloneis the right default for deep copying in modern JavaScript - it's native and handles edge cases well.updated
did you know it?WeakMap and WeakSet
Weak collections hold weak references to their keys/values - meaning they don't prevent garbage collection.
WeakMap
jsconst weakMap = new WeakMap(); let user = { name: 'Alice' }; weakMap.set(user, { sessionData: '...' }); weakMap.get(user); // { sessionData: '...' } user = null; // object can now be garbage collected // weakMap entry is automatically removedConstraints:
- Keys must be objects (or non-registered symbols since ES2023), not primitives.
- Not iterable - no
.forEach, no.size. - Only
.get,.set,.has,.delete.
WeakSet
jsconst visited = new WeakSet(); function process(node) { if (visited.has(node)) return; // already processed visited.add(node); // ... }When to use
Storing metadata about objects without preventing GC
jsconst cache = new WeakMap(); function process(element) { if (cache.has(element)) return cache.get(element); const result = heavyComputation(element); cache.set(element, result); return result; } // when element is removed from DOM and dereferenced, // cache entry is cleaned up automaticallyTracking without memory leaks
jsconst listeners = new WeakMap(); // safe to store per-element event data - cleans up with the elementvs regular Map/Set
MapWeakMapKey types Any Objects only Prevents GC Yes No Iterable Yes No Memory leak risk Yes No WeakMapis ideal for private data associated with DOM nodes or class instances - when the object goes away, the associated data disappears automatically.updated
did you know it?