Deep Dive: 1. JavaScript Fundamentals
1.1 Scope & Hoisting
1. Plain-language explanation
Scope determines where variables are visible (accessible) in your code. Hoisting is JavaScript's quirk where it "moves" variable and function declarations to the top of their scope before executing the code.
2. Why it exists / what problem it solves
During the compile phase, the JS engine allocates memory for all declarations before executing line-by-line. Hoisting allows you to call a function at the top of a file while defining it at the bottom, which can make code easier to read (top-down structure).
3. Correct usage
Always use let or const (which are block-scoped and prevent pre-initialization access) and structure your code logically.
// Hoisting allows this to work:
sayHi();
function sayHi() {
const name = "Alice"; // Block-scoped
console.log(name);
}4. The common mistake
Accessing variables before declaration using var. var declarations are hoisted and initialized as undefined. This masks errors that let would catch.
console.log(x); // Outputs: undefined (Doesn't crash!)
var x = 10;
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;5. How this would come up in an interview
Conceptual: You'll be shown a tricky code snippet with var or function re-declarations and asked "What does this output and why?".
Live-coding: If you use var in a modern React codebase, the interviewer will immediately flag it as a lack of modern JS knowledge.
6. Mock Q&A
Q (Easy): Explain the difference between var, let, and const.
A: var is function-scoped and hoisted with an initial value of undefined. let and const are block-scoped and hoisted but remain uninitialized in the "Temporal Dead Zone" (TDZ) until evaluated.
Q (Hard): What is the Temporal Dead Zone (TDZ)?
A: The time between entering a block scope and the actual execution of a let/const declaration. Accessing the variable during this window throws a ReferenceError.
1.2 Closures
1. Plain-language explanation
A closure is a function that remembers the variables from the place where it was defined, even after that place (the outer function) has finished running.
2. Why it exists / what problem it solves
Closures enable data privacy (encapsulation) in JavaScript. Before classes, closures were the primary way to create "private" variables. They are also the mechanism that makes functional programming patterns like currying and higher-order functions work.
3. Correct usage
Creating a function that "locks in" some configuration state.
function createMultiplier(multiplier) {
// The returned function forms a closure over 'multiplier'
return function(num) {
return num * multiplier;
}
}
const double = createMultiplier(2);
console.log(double(5)); // 104. The common mistake
Creating closures inside a loop using var, resulting in all closures sharing the same final variable state.
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Outputs: 4, 4, 4 (because `var` is function-scoped, the loop mutates the same `i`)
// Fix: use `let` (creates a new block scope per iteration)
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Outputs: 1, 2, 35. How this would come up in an interview
Conceptual: "What is a closure? Give an example."
Live-coding: You'll be asked to implement a function like once() (a function that can only be called once) or memoize().
6. Mock Q&A
Q (Easy): What is a closure? A: A closure is a function bundled together with its lexical environment. It gives a function access to its outer scope even after the outer function has returned.
Q (Hard): How do closures relate to memory leaks? A: If a closure holds onto a large object in its lexical scope and the closure itself is kept alive (e.g., attached to a long-lived DOM element event listener), the garbage collector cannot free that object, causing a leak.
1.3 this Binding Across Function Types
1. Plain-language explanation
this is a special keyword that refers to the "context" or "owner" of the currently executing code. Its value depends entirely on how a function is called, unless it's an arrow function.
2. Why it exists / what problem it solves
It allows functions to be reused against different object contexts. Instead of hardcoding object names, methods can dynamically reference the object they are attached to.
3. Correct usage
Using regular functions for object methods (so this points to the object), and arrow functions for nested callbacks (so this inherits from the method).
const user = {
name: "Alice",
greet() { // Regular function: 'this' depends on invocation
setTimeout(() => {
// Arrow function: 'this' is lexically inherited from greet()
console.log(`Hi, I'm ${this.name}`);
}, 100);
}
};
user.greet(); // "Hi, I'm Alice"4. The common mistake
Extracting a method from an object and passing it as a callback, which strips it of its this context.
const obj = {
val: 42,
printVal() { console.log(this.val); }
};
// Mistake: passing the function reference alone
setTimeout(obj.printVal, 100); // undefined (this is Window/Global)
// Fix: bind it, or use an wrapper arrow function
setTimeout(obj.printVal.bind(obj), 100);
setTimeout(() => obj.printVal(), 100);5. How this would come up in an interview
Conceptual: "Explain call, apply, and bind."
Live-coding: A React class component where an event handler throws an error because this.setState is undefined (requires .bind(this) or an arrow function).
6. Mock Q&A
Q (Easy): How does this differ in arrow functions vs regular functions?
A: Regular functions bind this dynamically based on how they are called (e.g., the object left of the dot). Arrow functions do not have their own this; they lexically inherit it from their enclosing scope.
Q (Hard): What is the difference between .call(), .apply(), and .bind()?
A: .call(context, arg1, arg2) invokes the function immediately with arguments. .apply(context, [args]) is identical but takes an array of arguments. .bind(context) doesn't invoke the function; it returns a new function with this permanently locked to the provided context.
1.4 Prototypes vs ES6 Classes
1. Plain-language explanation
JavaScript doesn't have traditional classes like Java. Instead, objects link directly to other objects via a hidden property called a "prototype." ES6 class syntax is just syntactic sugar that hides this wiring to look like traditional Object-Oriented Programming (OOP).
2. Why it exists / what problem it solves
Prototypes allow objects to share methods and properties in memory without duplicating them for every instance, saving memory and allowing inheritance.
3. Correct usage
Use ES6 classes for readability and cleaner inheritance, but understand that methods defined on the class end up on the prototype.
class Animal {
constructor(name) { this.name = name; }
speak() { console.log(`${this.name} makes a noise`); } // Lives on Animal.prototype
}
const dog = new Animal("Rex");
dog.speak();4. The common mistake
Thinking that classes in JS are structurally isolated. If you modify a class's prototype at runtime, it affects all existing instances immediately.
class User {}
const u1 = new User();
// Modifying the prototype affects instances created in the past!
User.prototype.sayHi = function() { console.log("Hi"); }
u1.sayHi(); // "Hi" - this wouldn't work in Java/C#5. How this would come up in an interview
Conceptual: "Explain prototypal inheritance."
Live-coding: You might be asked to implement an ES6 class, and then translate it back to ES5 constructor functions and ClassName.prototype.method syntax.
6. Mock Q&A
Q (Easy): Are ES6 classes the same as classes in Java? A: No, they are syntactic sugar over JavaScript's existing prototype-based inheritance. Under the hood, they are just constructor functions.
Q (Hard): What is the prototype chain?
A: When accessing a property on an object, JS first checks the object itself. If not found, it checks the object's __proto__. It traverses this chain up to Object.prototype, and finally null, returning undefined if the property is never found.
1.5 The Event Loop (Call Stack, Microtask vs Macrotask Queue)
1. Plain-language explanation
JavaScript is single-threaded (can only do one thing at a time). The Event Loop is the traffic cop that lets JS handle long-running tasks (like network requests) in the background, pausing them and picking them back up when they are ready, without freezing the main thread.
2. Why it exists / what problem it solves
If JS stopped and waited for every network request or timer to finish (blocking), the browser UI would freeze. The Event Loop allows asynchronous non-blocking behavior.
3. Correct usage
Offloading heavy I/O or timers to the Event Loop, while keeping the main call stack lean.
console.log("1. Sync");
setTimeout(() => console.log("4. Macrotask"), 0);
Promise.resolve().then(() => console.log("3. Microtask"));
console.log("2. Sync");
// Output: 1, 2, 3, 44. The common mistake
Assuming setTimeout(fn, 0) executes instantly. It doesn't; it schedules fn as a macrotask. Microtasks (Promises) will always execute before the next macrotask (setTimeout/setInterval).
// Mistake: Blocking the event loop entirely
while (true) {
// This traps the Call Stack forever. No microtasks or macrotasks will ever run.
// The browser tab crashes.
}5. How this would come up in an interview
Conceptual: "Explain the Event Loop."
Live-coding: You will be given a block of code with console.log, setTimeout, and Promise.then nested together and asked to write out the exact console output order.
6. Mock Q&A
Q (Easy): What is the difference between the Call Stack and the Task Queue? A: The Call Stack executes synchronous code directly. The Task Queue holds asynchronous callbacks that are waiting to be moved onto the empty Call Stack by the Event Loop.
Q (Hard): Which has higher priority: Microtasks or Macrotasks? A: Microtasks (Promises, MutationObserver). When the Call Stack empties, the Event Loop processes all Microtasks until the Microtask queue is totally empty, before it processes a single Macrotask (setTimeout).
1.6 Async/Await Desugaring
1. Plain-language explanation
async/await is a cleaner way to write Promise-based code. Instead of .then().catch(), you write code that looks synchronous. Under the hood, it's just syntax sugar over Promises and Generator functions.
2. Why it exists / what problem it solves
It solves "Promise Hell" (deeply nested .then chains) and makes asynchronous control flow (like if/for loops over async data) vastly easier to read and write.
3. Correct usage
Awaiting a Promise to resolve before moving to the next line, wrapped in try/catch.
async function fetchUser() {
try {
const res = await fetch('/api/user');
const user = await res.json();
return user;
} catch (error) {
console.error("Failed to fetch", error);
}
}4. The common mistake
Using await inside a synchronous loop callback like .map() or .forEach().
// Mistake: This does NOT wait for the API calls.
// forEach is synchronous; it fires all fetches immediately and finishes.
ids.forEach(async (id) => {
await fetch(`/item/${id}`);
});
console.log("Done"); // Logs immediately, before fetches finish.
// Fix: Use a standard `for...of` loop
for (const id of ids) {
await fetch(`/item/${id}`);
}5. How this would come up in an interview
Conceptual: "How does async/await work under the hood?"
Live-coding: You might be asked to refactor old .then() chain code to async/await, or fix a bug where multiple API calls are waterfalling slowly instead of running concurrently.
6. Mock Q&A
Q (Easy): What does an async function return?
A: It always returns a Promise. If you return a primitive value, it is automatically wrapped in a resolved Promise.
Q (Hard): If await blocks the execution of the function, doesn't it block the thread?
A: No. await pauses the execution of that specific function, yielding control back to the Event Loop to execute other code. It does not block the main JS thread.
1.7 Promises and Combinators
1. Plain-language explanation
A Promise is an object representing the eventual completion (or failure) of an asynchronous operation. Combinators (like Promise.all) are utility methods to handle multiple promises simultaneously.
2. Why it exists / what problem it solves
Promises replace legacy callback patterns ("Callback Hell"), providing a standardized way to handle async success/failure states. Combinators solve the problem of running async tasks in parallel.
3. Correct usage
Using Promise.all to fetch parallel data instead of awaiting sequentially.
// Fetches happen at the same time
const [users, posts] = await Promise.all([
fetch('/users').then(r => r.json()),
fetch('/posts').then(r => r.json())
]);4. The common mistake
Using Promise.all when some tasks might fail, but you don't want the whole batch to fail. Promise.all rejects immediately if any promise rejects ("fail-fast").
// Mistake: If fetch('/bad') fails, the whole try block crashes,
// and we don't get the result of fetch('/good').
const results = await Promise.all([ fetch('/good'), fetch('/bad') ]);
// Fix: Use Promise.allSettled
const results = await Promise.allSettled([ fetch('/good'), fetch('/bad') ]);
// returns [{ status: 'fulfilled', value: ...}, { status: 'rejected', reason: ...}]5. How this would come up in an interview
Conceptual: "What are the states of a Promise?" (Pending, Fulfilled, Rejected).
Live-coding: "Write a function that fetches from 3 APIs at the same time, but if API 3 fails, it shouldn't crash the whole function." (Requires Promise.allSettled or individual try/catch).
6. Mock Q&A
Q (Easy): What is the difference between Promise.all and Promise.race?
A: Promise.all waits for all promises to fulfill (or the first to reject). Promise.race settles as soon as the first promise settles, whether it fulfills or rejects.
Q (Hard): What is Promise.any?
A: Added in ES2021, it takes an iterable of promises and returns a single promise that fulfills as soon as any of the promises fulfills. It only rejects if all promises reject.
1.8 Map/Set vs Plain Objects/Arrays
1. Plain-language explanation
Map and Set are modern data structures. A Map is like an Object but allows any data type as a key. A Set is like an Array but automatically removes duplicates.
2. Why it exists / what problem it solves
Plain Objects convert all keys to strings (e.g., obj[{}] = 1 sets the key "[object Object]"). Maps fix this and guarantee insertion order. Sets replace the clunky pattern of checking array.includes(x) before pushing.
3. Correct usage
Using a Set to deduplicate an array, and a Map to cache data associated with an object reference.
const uniqueTags = [...new Set(["js", "react", "js"])]; // ["js", "react"]
// Map example
const cache = new Map();
const userObj = { id: 1 };
cache.set(userObj, { lastLogin: Date.now() }); // The object itself is the key!4. The common mistake
Assuming Set will deduplicate objects with the same properties.
const users = new Set([ {id: 1}, {id: 1} ]);
console.log(users.size); // 2!
// Sets check for strict reference equality (===). Two different objects in memory are not considered duplicates.5. How this would come up in an interview
Conceptual: "When would you use a Map over an Object?"
Live-coding: A problem requiring fast lookups. Array includes() is O(n), whereas Set has() is O(1). If an interviewer asks you to optimize a slow double-loop array search, they want you to put the data in a Set or Map.
6. Mock Q&A
Q (Easy): How do you remove all duplicates from an array?
A: Array.from(new Set(array)) or [...new Set(array)].
Q (Hard): What are the new Set methods in ES2024?
A: ES2024 added native set theory methods like setA.union(setB), setA.intersection(setB), and setA.difference(setB).
1.9 Array/Object Methods
1. Plain-language explanation
Built-in functions to traverse and manipulate data structures. Array methods (map, filter, reduce) iterate over lists, while Object methods (keys, values, entries, groupBy) inspect objects.
2. Why it exists / what problem it solves
They enable functional, declarative programming. Instead of writing verbose for loops and manually mutating variables, you declare what you want to happen.
3. Correct usage
Chaining methods to transform data without mutating the original array.
const users = [{ age: 15 }, { age: 25 }, { age: 30 }];
const adultAges = users
.filter(u => u.age >= 18)
.map(u => u.age); // [25, 30]
// Modern ES2024 Grouping
const groupedByAdult = Object.groupBy(users, u => u.age >= 18 ? 'adult' : 'minor');4. The common mistake
Using .map() when you don't care about the returned array (use .forEach() instead), or mutating the array during a sort.
const arr = [3, 1, 2];
const sorted = arr.sort(); // Mutates the original array! arr is now [1, 2, 3]
// Fix: ES2023 toSorted() creates a new array
const safeSorted = arr.toSorted();
// Older way: [...arr].sort()5. How this would come up in an interview
Conceptual: "Explain how reduce works."
Live-coding: Literally any data transformation task. You will be expected to fluently chain filter/map/reduce to transform a mock API response into UI props.
6. Mock Q&A
Q (Easy): What does Object.entries(obj) return?
A: An array of the object's own enumerable string-keyed property [key, value] pairs.
Q (Hard): How do you flatten a deeply nested array?
A: Using array.flat(Infinity). Before flat(), you had to write a recursive reduce function.
1.10 Destructuring & Spread/Rest
1. Plain-language explanation
Syntax to unpack data (destructuring), expand data into individual elements (spread ...), or bundle remaining elements into an array/object (rest ...).
2. Why it exists / what problem it solves
It vastly reduces boilerplate code. Instead of const name = user.name; const age = user.age;, you can do it in one line. Spread makes copying and merging immutable state (crucial for React) much cleaner than Object.assign().
3. Correct usage
Extracting props, creating shallow copies, and updating state immutably.
const user = { name: "Om", role: "Dev", age: 25 };
// Destructuring & Rest
const { name, ...otherDetails } = user;
// Spread to immutably update
const updatedUser = { ...user, age: 26 };4. The common mistake
Assuming spread creates a deep copy. It only creates a shallow copy. Nested objects share the exact same memory reference.
const original = { nested: { val: 1 } };
const copy = { ...original };
copy.nested.val = 99;
console.log(original.nested.val); // 99! The nested object was mutated.
// Fix: structuredClone(original) for a true deep copy.5. How this would come up in an interview
Conceptual: "What is the difference between spread and rest?"
Live-coding: In React, if you update an object state using setState(state.val = 2), the interviewer will ask you to fix it using spread syntax.
6. Mock Q&A
Q (Easy): Differentiate Spread and Rest.
A: Spread expands an iterable into individual elements (e.g., Math.max(...arr)). Rest collects multiple elements and condenses them into a single array/object (e.g., function fn(...args)).
1.11 Implementing Debounce/Throttle from Scratch
(See the 5-15 minute build exercise section at the end)
1. Plain-language explanation
Debounce: "Wait until I stop doing this for X milliseconds, then execute." (Like an elevator door waiting for people to stop entering). Throttle: "Only execute this once every X milliseconds, no matter how many times I try." (Like a machine gun firing at a fixed rate).
2. Why it exists / what problem it solves
Prevents performance issues. If you attach an API call to a search input's onChange, you will fire hundreds of requests. Debouncing waits for the user to stop typing.
4. The common mistake
Implementing debounce but failing to manage the closure over the timerId, or failing to pass arguments (...args) and this context to the inner function.
1.12 == vs === and Coercion
1. Plain-language explanation
=== (strict equality) checks if two values are identical in both type and value. == (loose equality) attempts to convert the values to the same type (coercion) before comparing them.
2. Why it exists / what problem it solves
== was originally designed to be forgiving (e.g., "42" == 42), but JS's coercion rules are notoriously complex and lead to silent bugs.
3. Correct usage
Always use ===. The only somewhat acceptable use of == is val == null, which checks if val is either null or undefined in one step.
4. The common mistake
Writing complex logic relying on coercion that results in WTFs.
console.log(0 == false); // true
console.log("" == false); // true
console.log([] == false); // true
console.log([] == ""); // true
// Use ===.5. How this would come up in an interview
Conceptual: "What is the difference between loose and strict equality?" or "What are falsy values in JS?"
Live-coding: A bug in a React component where 0 is rendered incorrectly because of a count && <Component /> check (since 0 is falsy, React renders 0).
6. Mock Q&A
Q (Easy): Name the falsy values in JavaScript.
A: false, 0, -0, "" (empty string), null, undefined, and NaN.
1.13 CommonJS vs ES Modules
1. Plain-language explanation
These are the two ways JavaScript files share code. CommonJS (CJS) uses require() and module.exports. ES Modules (ESM) use import and export.
2. Why it exists / what problem it solves
CJS was created for Node.js backend environments. ESM is the modern, official standard for JavaScript across both the browser and backend.
3. Correct usage
In modern Node (or any frontend framework), use ESM for static analysis and tree-shaking capabilities.
// ESM
export const util = () => {};
import { util } from './util.js';4. The common mistake
Trying to mix require and import in the same Node.js project without configuring package.json with "type": "module".
5. Mock Q&A
Q (Hard): What is the functional difference between require and import?
A: require is synchronous and dynamic (can be called inside if statements). import is asynchronous and static (must be at the top level), allowing build tools like Webpack to analyze imports and remove unused code ("tree-shaking").
1.14 Error Handling Patterns
1. Plain-language explanation
How you intercept, log, and recover from code failures instead of letting the application crash entirely.
2. Why it exists / what problem it solves
Network requests fail, databases timeout. Error handling prevents the end-user from seeing blank screens or exposing sensitive stack traces.
3. Correct usage
async function getData() {
try {
const data = await riskyOperation();
} catch (error) {
if (error instanceof NetworkError) {
// handle specific error
} else {
throw error; // bubble up unknown errors
}
} finally {
setLoading(false); // Always runs, fail or succeed
}
}4. The common mistake
Catching an error and silently swallowing it without logging or throwing, leaving the application in an unpredictable ghost state.
Live-Coding Exercise: Implement Debounce (Topic 1.11)
Spec:
Write a function debounce(func, wait) that takes a function func and a delay wait in milliseconds. It should return a new function that, when invoked, delays the execution of func until after wait milliseconds have elapsed since the last time the returned function was invoked.
Requirements:
- It must pass arguments to the original function.
- It must preserve the
thiscontext.
Do not scroll down until you have tried!
<br><br><br><br><br><br><br><br><br><br>
<details> <summary><strong>View Reference Solution</strong></summary>function debounce(func, wait) {
let timeoutId = null;
// We return a regular function (not arrow) so we can capture 'this' from the caller
return function(...args) {
// 1. Clear the existing timer if the user typed again
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
// 2. Set a new timer
timeoutId = setTimeout(() => {
// 3. Invoke the original function with the correct context and arguments
func.apply(this, args);
// Cleanup (optional but good practice)
timeoutId = null;
}, wait);
};
}
// Usage test:
const myObj = {
name: "Om",
speak(message) {
console.log(`${this.name} says: ${message}`);
}
}
const debouncedSpeak = debounce(myObj.speak, 1000);
// Because we used a regular function in the return statement and `apply(this)`,
// `debouncedSpeak` correctly bounds to `myObj` when called as an object method.
myObj.debouncedSpeak = debouncedSpeak;
myObj.debouncedSpeak("Hello");
myObj.debouncedSpeak("World");
// Only "Om says: World" will print, exactly 1 second after this last call.