The JavaScript Interview Bible
The Deepest Guide Ever Written — From Engine Internals to Production War Stories
This isn't a list of questions. This is the book that teaches you HOW JavaScript actually works so deeply that no question can surprise you. Every concept is explained the way you'd explain it to a colleague at a whiteboard — with the "why behind the why," real production stories, engine-level truth, and code you can run.
TABLE OF CONTENTS
PART I — THE ENGINE ROOM: How JavaScript Actually Runs
- Chapter 1: Execution Context — The Hidden Backbone
- Chapter 2: Scope Chain & Lexical Environment
- Chapter 3: The Call Stack — Visualized
- Chapter 4: Memory Management & Garbage Collection
- Chapter 5: The Event Loop — The Full, Honest Truth
PART II — THE TYPE SYSTEM: Every Edge Case That Exists
- Chapter 6: Primitives — Deeper Than You Think
- Chapter 7: Type Coercion — The Complete Algorithm
- Chapter 8: == vs === — The Actual Specification Steps
- Chapter 9: typeof, instanceof, and Their Traps
PART III — FUNCTIONS: The Heart of the Language
- Chapter 10: Functions Are Objects (Really)
- Chapter 11: Closures — The Full Mental Model
- Chapter 12:
this— Every Single Rule - Chapter 13: call, apply, bind — Build Them Yourself
- Chapter 14: Higher-Order Functions & Functional Patterns
PART IV — OBJECTS & PROTOTYPES: The Real Inheritance
- Chapter 15: The Prototype Chain — What
classIs Hiding - Chapter 16: Property Descriptors & Object.defineProperty
- Chapter 17: Composition vs Inheritance — The Real Debate
- Chapter 18: Proxy & Reflect — Metaprogramming
PART V — ASYNC MASTERY: Beyond the Basics
- Chapter 19: Promises — Build One From Scratch
- Chapter 20: async/await — The Traps Nobody Warns You About
- Chapter 21: Generators & Iterators — The Protocol
- Chapter 22: Advanced Async Patterns (Race Conditions, Cancellation, Retry)
PART VI — ADVANCED WEAPONS
- Chapter 23: Symbol & Well-Known Symbols
- Chapter 24: WeakMap, WeakSet, WeakRef, FinalizationRegistry
- Chapter 25: Tagged Template Literals
- Chapter 26: Regex in JavaScript — Actually Useful
- Chapter 27: Modules Deep Dive — Static vs Dynamic Import
PART VII — THE BROWSER: DOM, Events & APIs
- Chapter 28: Event Bubbling, Capturing & Delegation
- Chapter 29: Critical Rendering Path
- Chapter 30: Web Storage, IndexedDB, Cookies
- Chapter 31: Web Workers & Service Workers
- Chapter 32: IntersectionObserver, MutationObserver, ResizeObserver
PART VIII — PATTERNS & ARCHITECTURE
- Chapter 33: Design Patterns in JavaScript
- Chapter 34: Error Handling — A Production Strategy
- Chapter 35: Security — XSS, Prototype Pollution, and More
PART IX — PERFORMANCE
- Chapter 36: The 4 Types of Memory Leaks
- Chapter 37: Debounce, Throttle, and requestAnimationFrame
- Chapter 38: Lazy Loading, Code Splitting, Tree Shaking
PART X — 50 CODING CHALLENGES
- Challenges 1–15: Foundation (Junior)
- Challenges 16–35: Intermediate (Mid-Level)
- Challenges 36–50: Advanced (Senior/Staff)
PART I — THE ENGINE ROOM
Chapter 1: Execution Context — The Hidden Backbone
Every time JavaScript runs a piece of code, it creates an invisible wrapper called an Execution Context. Understanding this is like understanding the foundation of a building — you never see it, but everything stands on it.
What Is an Execution Context?
Think of it as a box that holds three things:
- Variable Environment — all the variables, functions, and arguments declared in this scope
- Scope Chain — a link to the parent context (how inner functions "see" outer variables)
thisbinding — whatthisrefers to in this context
There are three types of execution contexts:
Global Execution Context (GEC): Created when your script first loads. In a browser, its this is the window object. In Node.js, it's the global object. There's only ever one of these.
Function Execution Context (FEC): Created every time a function is called. Each call gets its own context — even if the same function is called twice.
Eval Execution Context: Created when code runs inside eval(). We ignore this because you should never use eval().
The Two Phases
Every execution context goes through two phases:
Phase 1 — Creation Phase (before your code runs a single line):
JavaScript scans through the code and:
- Allocates memory for all
vardeclarations (set toundefined) - Allocates memory for all function declarations (stored in full)
- Sets up the scope chain
- Determines the value of
this letandconstdeclarations are noted but NOT initialized (they enter the Temporal Dead Zone)
This is hoisting. It's not "moving" code — it's the engine setting up memory before execution begins.
Phase 2 — Execution Phase:
JavaScript runs your code line by line, assigning values and executing logic.
// Let's trace what the engine ACTUALLY does:
console.log(team); // ?
console.log(greet); // ?
console.log(score); // ?
var team = 'Alpha';
let score = 100;
function greet() {
return 'Hello';
}
// CREATION PHASE sets up:
// team → undefined (var is hoisted with undefined)
// greet → function greet() { return 'Hello' } (fully hoisted)
// score → TDZ (let is hoisted but NOT initialized)
// EXECUTION PHASE runs line by line:
// Line 1: console.log(team) → undefined
// Line 2: console.log(greet) → [Function: greet]
// Line 3: console.log(score) → ReferenceError! (TDZ)
// Script STOPS — lines after the error never run
Why This Matters in Real Life
You're debugging a React app. A function works when defined with function, but breaks when you refactor it to const. Now you know why — the component tried to use it before the const line, and const doesn't hoist the same way.
Interview Question: Trace the Execution
var x = 1;
function outer() {
console.log(x); // ?
var x = 2;
console.log(x); // ?
function inner() {
console.log(x); // ?
var x = 3;
console.log(x); // ?
}
inner();
console.log(x); // ?
}
outer();
console.log(x); // ?
Answer: undefined, 2, undefined, 3, 2, 1
Each function creates its OWN execution context. The var x inside outer shadows the global x. The var x inside inner shadows the outer x. During each function's creation phase, its local var x is hoisted to undefined, which is what the first console.log in each function sees.
Chapter 2: Scope Chain & Lexical Environment
The Scope Chain Explained with a Building Metaphor
Imagine a 5-story building. Each floor is a scope. When you need something (a variable), you first check your own floor. If it's not there, you go down one floor. Then the next. All the way to the ground floor (global scope). You never go UP — only down.
const building = 'Global Floor'; // Ground floor
function floor1() {
const office = 'Floor 1 Office';
function floor2() {
const desk = 'Floor 2 Desk';
function floor3() {
// Needs: desk, office, building
console.log(desk); // Found on Floor 2 ✓
console.log(office); // Found on Floor 1 ✓
console.log(building); // Found on Ground Floor ✓
console.log(basement); // ReferenceError — doesn't exist anywhere
}
floor3();
}
floor2();
}
floor1();
Lexical vs Dynamic Scope
JavaScript uses lexical (static) scope — the scope is determined by WHERE the function is WRITTEN in the code, not where it's called from.
const name = 'Global';
function printName() {
console.log(name);
}
function wrapper() {
const name = 'Wrapper';
printName(); // What prints?
}
wrapper(); // "Global" — NOT "Wrapper"
Why "Global"? Because printName was WRITTEN in the global scope. Its scope chain is printName → global. It doesn't matter that it was CALLED from inside wrapper. The scope chain is set when the function is created (written), not when it's invoked.
This is the foundation of closures. Memorize this: a function's scope chain is baked in at creation time.
Block Scope vs Function Scope
function example() {
// Function scope — var lives here
var funcScoped = 'I exist in the entire function';
if (true) {
// Block scope — let/const live here
let blockScoped = 'I only exist in this if-block';
var stillFuncScoped = 'I leak out of this block';
}
console.log(funcScoped); // ✓ accessible
console.log(stillFuncScoped); // ✓ accessible (var ignores blocks)
console.log(blockScoped); // ReferenceError (let stays in block)
}
The Scope Chain in Closures — The Engine View
When a function is created, the engine attaches an internal property called [[Environment]] that points to the scope where the function was born. This is a direct reference to the parent's variable storage, not a copy. That's why closures work — the inner function holds a living reference to the outer scope's variables.
function createMultiplier(factor) {
// factor = 5 is stored in this execution context
// When we return the inner function, it carries a [[Environment]]
// pointer back to THIS context
return function(number) {
// This function was born inside createMultiplier's scope
// Its [[Environment]] points to { factor: 5 }
return number * factor;
};
}
const double = createMultiplier(2); // [[Environment]] → { factor: 2 }
const triple = createMultiplier(3); // [[Environment]] → { factor: 3 }
// createMultiplier has finished running, but its context is NOT
// garbage collected because double and triple still reference it
double(10); // 20
triple(10); // 30
Chapter 3: The Call Stack — Visualized
The call stack is JavaScript's to-do list. It's a LIFO (Last In, First Out) data structure. Functions go on top when called, and come off the top when they return.
Stack Overflow — How It Happens
function chicken() {
return egg();
}
function egg() {
return chicken();
}
chicken();
// RangeError: Maximum call stack size exceeded
The stack grows with every call but nothing ever returns, so it keeps growing until the engine says "enough" (typically around 10,000–25,000 frames depending on the engine).
Real-World Stack Trace Reading
When you see an error in production:
TypeError: Cannot read property 'name' of undefined
at formatUser (utils.js:42)
at processOrder (orders.js:18)
at handleSubmit (form.js:7)
at HTMLButtonElement.onclick (index.html:15)
Read it bottom to top: the user clicked a button → handleSubmit was called → which called processOrder → which called formatUser → which crashed because something was undefined. The stack trace IS the call stack at the moment of the crash.
Chapter 4: Memory Management & Garbage Collection
How JavaScript Manages Memory
JavaScript handles memory automatically, but "automatic" doesn't mean "perfect." Understanding this prevents some of the worst production bugs.
The lifecycle:
- Allocate — you create a variable, the engine finds memory for it
- Use — you read and write the variable
- Release — when the variable is no longer reachable, the Garbage Collector (GC) frees the memory
Mark-and-Sweep — How the GC Works
The GC starts from "roots" (global object, currently executing functions, etc.) and follows every reference chain. Anything it can reach is "alive." Anything it can't reach is "garbage" and gets freed.
function processData() {
const hugeArray = new Array(1000000).fill('data');
// hugeArray is reachable here — GC won't touch it
const result = hugeArray.length;
return result;
}
// After processData() returns, hugeArray is no longer reachable
// from any root. GC will eventually free it.
The 4 Types of Memory Leaks (Senior-Level Knowledge)
Leak Type 1: Accidental Globals
function handleData() {
results = []; // No const/let/var — this is now window.results!
// It will NEVER be garbage collected because globals are always reachable
}
Leak Type 2: Forgotten Timers and Callbacks
// This interval runs FOREVER, and the closure keeps `hugeData` alive
const hugeData = loadMassiveDataset();
setInterval(() => {
const element = document.getElementById('status');
if (element) {
element.textContent = hugeData.summary;
}
// Even if #status is removed from the DOM, this interval keeps running
// and keeps hugeData in memory
}, 5000);
// FIX: Store the interval ID and clear it when done
const intervalId = setInterval(/* ... */);
// Later:
clearInterval(intervalId);
Leak Type 3: Detached DOM References
const elements = {};
function addButton() {
const btn = document.createElement('button');
document.body.appendChild(btn);
elements.myButton = btn; // JS reference stored
}
function removeButton() {
document.body.removeChild(elements.myButton);
// The DOM node is removed from the page BUT
// elements.myButton still references it in JS memory!
// The GC can't free it.
// FIX: Also delete the JS reference
elements.myButton = null;
}
Leak Type 4: Closures Holding Onto Large Scopes
function createProcessor() {
const cache = new Map(); // Grows forever
const hugeBuffer = Buffer.alloc(1024 * 1024 * 50); // 50MB
return function process(key, data) {
cache.set(key, data);
// cache is NEVER cleared — it grows with every call
// hugeBuffer is captured by the closure even if process() never uses it
};
}
// FIX: Only capture what you need, and implement cache eviction
function createProcessor() {
const cache = new Map();
const MAX_CACHE = 1000;
return function process(key, data) {
if (cache.size >= MAX_CACHE) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(key, data);
};
}
Chapter 5: The Event Loop — The Full, Honest Truth
Most guides show you the simple version. This is the complete version.
The Architecture
┌───────────────────────────────────────────────┐
│ Call Stack │
│ (your synchronous code runs here, one │
│ function at a time) │
└─────────────────────┬─────────────────────────┘
│ When stack is empty,
│ the Event Loop checks:
▼
┌─────────────────────────────────────────────────┐
│ 1. MICROTASK QUEUE (drain ALL before moving on)│
│ - Promise.then / .catch / .finally │
│ - queueMicrotask() │
│ - MutationObserver callbacks │
│ - await (resumes after await are microtasks)│
└─────────────────────┬───────────────────────────┘
│ All microtasks drained?
▼
┌─────────────────────────────────────────────────┐
│ 2. RENDER STEPS (if browser, ~every 16.67ms) │
│ - requestAnimationFrame callbacks │
│ - Style calculation │
│ - Layout │
│ - Paint │
└─────────────────────┬───────────────────────────┘
│ Render done (or skipped)?
▼
┌─────────────────────────────────────────────────┐
│ 3. MACROTASK QUEUE (pick ONE, then loop back) │
│ - setTimeout / setInterval │
│ - setImmediate (Node.js) │
│ - I/O callbacks │
│ - UI events (click, scroll, keydown) │
│ - MessageChannel │
└─────────────────────────────────────────────────┘
The critical rule: All microtasks drain completely before any macrotask runs. And between macrotasks, all microtasks drain again. This means a microtask that queues another microtask will run BEFORE the next setTimeout — even if that setTimeout's delay is 0.
The Ultimate Event Loop Question
Predict the exact output order:
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => console.log('5'), 0);
});
Promise.resolve().then(() => console.log('6'));
setTimeout(() => console.log('7'), 0);
console.log('8');
Answer: 1, 8, 4, 6, 2, 3, 7, 5
Trace:
1— synchronous, runs immediatelysetTimeout(() => '2'...)— macrotask queued (M1)Promise.then(() => '4'...)— microtask queued (m1)Promise.then(() => '6')— microtask queued (m2)setTimeout(() => '7')— macrotask queued (M2)8— synchronous, runs immediately
Stack is now empty. Drain microtask queue:
7. m1 runs: 4 prints. It schedules setTimeout(() => '5') (M3).
8. m2 runs: 6 prints.
Microtask queue is empty. Pick next macrotask:
9. M1 runs: 2 prints. It schedules Promise.then(() => '3') (m3).
Drain microtasks before next macrotask: m3 runs: 3 prints.
Pick next macrotask:
10. M2 runs: 7 prints.
Pick next macrotask:
11. M3 runs: 5 prints.
requestAnimationFrame — Where Does It Fit?
requestAnimationFrame (rAF) is not a macrotask and not a microtask. It runs during the render step, AFTER microtasks but BEFORE the next macrotask:
setTimeout(() => console.log('setTimeout'), 0);
requestAnimationFrame(() => console.log('rAF'));
Promise.resolve().then(() => console.log('Promise'));
// Typical output: Promise, rAF, setTimeout
// (but rAF timing depends on the browser's paint schedule)
Node.js Event Loop — The Differences
Node.js has a more complex event loop with 6 phases: timers → pending callbacks → idle/prepare → poll → check → close callbacks. The key difference is process.nextTick(), which runs BEFORE Promise microtasks:
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
// Node.js output: nextTick, Promise
// (nextTick queue drains before the Promise microtask queue)
PART II — THE TYPE SYSTEM
Chapter 6: Primitives — Deeper Than You Think
The 7 Primitive Types
string → 'hello', "world", `template`
number → 42, 3.14, Infinity, NaN, -0
boolean → true, false
null → null (intentional absence)
undefined → undefined (unintentional absence / not yet assigned)
symbol → Symbol('description') (unique, immutable identifier)
bigint → 42n, 9007199254740993n (arbitrary precision integers)
The Weird Parts Nobody Teaches
-0 exists and has real uses:
const negZero = -0;
negZero === 0; // true (=== lies here!)
Object.is(negZero, 0); // false (Object.is tells the truth)
1 / negZero; // -Infinity
1 / 0; // Infinity
// Real use: tracking direction in animations
// velocity of -0 means "moving left but stopped"
// velocity of 0 means "moving right but stopped"
NaN — the only value not equal to itself:
NaN === NaN; // false (this is by IEEE 754 specification)
Number.isNaN(NaN); // true (correct way to check)
isNaN('hello'); // true (WRONG — coerces string first)
Number.isNaN('hello'); // false (correct — no coercion)
Autoboxing — how primitives have methods:
const str = 'hello';
str.toUpperCase(); // 'HELLO' — but str is a primitive, not an object!
// Behind the scenes, the engine does:
// 1. Creates a temporary String object: new String('hello')
// 2. Calls .toUpperCase() on it
// 3. Returns 'HELLO'
// 4. Throws away the temporary object
// This is why this FAILS:
str.customProp = 'test';
console.log(str.customProp); // undefined
// The temporary object was discarded
typeof — The Complete Results Table
typeof undefined // "undefined"
typeof null // "object" ← FAMOUS BUG (since JS 1.0, can never be fixed)
typeof true // "boolean"
typeof 42 // "number"
typeof 'hello' // "string"
typeof Symbol() // "symbol"
typeof 42n // "bigint"
typeof {} // "object"
typeof [] // "object" ← arrays are objects
typeof function(){} // "function" ← special case, actually an object
typeof NaN // "number" ← NaN is a number 🤯
The reliable type check:
function getType(value) {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
return typeof value;
}
Chapter 7: Type Coercion — The Complete Algorithm
This is what separates a JavaScript developer who "knows" the language from one who truly UNDERSTANDS it.
The Three Coercion Types
JavaScript converts values using three abstract operations:
ToPrimitive(input) — converts objects to primitives:
- If input has
[Symbol.toPrimitive](), call it - Otherwise, try
valueOf(), thentoString()(for numbers) - Or try
toString(), thenvalueOf()(for strings)
ToNumber(input):
Number(undefined) // NaN
Number(null) // 0
Number(true) // 1
Number(false) // 0
Number('') // 0
Number(' ') // 0 (whitespace strings → 0)
Number('123') // 123
Number('123abc') // NaN
Number([]) // 0 ([] → '' → 0)
Number([5]) // 5 ([5] → '5' → 5)
Number([1,2]) // NaN ([1,2] → '1,2' → NaN)
Number({}) // NaN
ToString(input):
String(undefined) // 'undefined'
String(null) // 'null'
String(true) // 'true'
String(false) // 'false'
String(0) // '0'
String(-0) // '0' (not '-0'!)
String([1,2,3]) // '1,2,3'
String({}) // '[object Object]'
The + Operator — The Full Rules
The + operator is special because it does BOTH addition and concatenation:
Rule 1: If either operand is a string, convert the other to a string and concatenate. Rule 2: Otherwise, convert both to numbers and add. Rule 3: Objects get ToPrimitive first (which usually gives a string via toString).
// String wins — concatenation
'5' + 3 // '53'
3 + '5' // '35'
'5' + null // '5null'
'5' + undefined // '5undefined'
// Both numbers — addition
5 + 3 // 8
5 + null // 5 (null → 0)
5 + undefined // NaN (undefined → NaN)
// Objects — ToPrimitive first
5 + {} // '5[object Object]'
[] + [] // '' (both become '' then concatenate)
[] + {} // '[object Object]'
{} + [] // 0 (ONLY in console — {} is parsed as empty block, +[] is 0)
// The most confusing one:
true + true // 2 (both → 1, then 1 + 1)
true + '1' // 'true1' (string wins)
Chapter 8: == vs === — The Actual Specification
The === Algorithm (Strict Equality)
Simple: if the types are different, return false. If the types are the same, compare values. Done.
The only special case: NaN !== NaN.
The == Algorithm (Abstract Equality) — Step by Step
This is the exact algorithm from the ECMAScript specification:
1. If types are the same → use ===
2. null == undefined → true
3. undefined == null → true
4. number == string → convert string to number, then compare
5. boolean == anything → convert boolean to number, then compare
6. object == primitive → convert object via ToPrimitive, then compare
Let's trace some infamous examples:
// Example 1: [] == false
// Step: boolean == anything → convert boolean to number
// false → 0, so now: [] == 0
// Step: object == primitive → ToPrimitive([])
// [].toString() → '', so now: '' == 0
// Step: string == number → ToNumber('')
// '' → 0, so now: 0 == 0
// Result: true ✓
// Example 2: [] == ![]
// First, ![] is evaluated: [] is truthy, so ![] → false
// Now: [] == false → (same as Example 1) → true ✓
// An array is "equal" to its own negation!
// Example 3: '' == 0
// string == number → ToNumber('')
// '' → 0, so: 0 == 0
// Result: true ✓
// Example 4: '0' == false
// boolean == anything → ToNumber(false) → 0
// '0' == 0 → string == number → ToNumber('0') → 0
// 0 == 0 → true ✓
// But: '' == false → true AND '0' == false → true
// Yet: '' == '0' → false! (same type, different strings)
// Transitivity is BROKEN with ==
When == Is Actually Useful
There's exactly one legitimate use: checking for null OR undefined in one comparison.
// Without == (verbose)
if (value === null || value === undefined) {
// ...
}
// With == (clean, and this IS safe)
if (value == null) {
// catches both null and undefined, nothing else
}
The Airbnb guide still says use === everywhere for consistency. But knowing that == null is safe is senior-level knowledge.
Chapter 9: instanceof and Its Traps
How instanceof Works
instanceof walks up the prototype chain of the left side, checking if any prototype matches the right side's .prototype:
class Animal {}
class Dog extends Animal {}
const rex = new Dog();
rex instanceof Dog; // true — Dog.prototype is in rex's chain
rex instanceof Animal; // true — Animal.prototype is also in rex's chain
rex instanceof Object; // true — Object.prototype is at the end of every chain
The Traps
Trap 1: Primitives always return false:
'hello' instanceof String; // false — primitives aren't objects
42 instanceof Number; // false
// vs
new String('hello') instanceof String; // true — this IS an object
Trap 2: Cross-frame/realm issues:
// If you receive an array from an iframe or a different Node.js module
// (like via JSON.parse in a worker), instanceof may fail because
// each realm has its own Array constructor
const arr = iframe.contentWindow.eval('[]');
arr instanceof Array; // false!
Array.isArray(arr); // true — this is the reliable way
Trap 3: You can fake it with Symbol.hasInstance:
class EvenNumber {
static [Symbol.hasInstance](num) {
return typeof num === 'number' && num % 2 === 0;
}
}
4 instanceof EvenNumber; // true
5 instanceof EvenNumber; // false
PART III — FUNCTIONS
Chapter 10: Functions Are Objects
In JavaScript, functions are first-class objects. They can have properties, be passed as arguments, and be returned from other functions.
function greet(name) {
return `Hello, ${name}`;
}
// Functions have properties
console.log(greet.name); // 'greet'
console.log(greet.length); // 1 (number of parameters)
// You can add custom properties
greet.callCount = 0;
function trackedGreet(name) {
trackedGreet.callCount += 1;
return greet(name);
}
// Functions have a prototype property (used when called with new)
console.log(greet.prototype); // {} (every function gets one)
arguments vs rest — The Full Story
function oldWay() {
console.log(arguments); // { 0: 'a', 1: 'b', length: 2 } — array-LIKE
console.log(arguments.map); // undefined — it's NOT an array!
// To use array methods, you had to convert it:
const args = Array.prototype.slice.call(arguments);
}
function newWay(...args) {
console.log(args); // ['a', 'b'] — a REAL array
console.log(args.map); // [Function: map] — all array methods work
}
// Arrow functions don't have `arguments` at all:
const arrowFn = () => {
console.log(arguments); // ReferenceError (or captures outer arguments)
};
Chapter 11: Closures — The Full Mental Model
The Definition That Actually Makes Sense
A closure is not complicated. A closure happens when:
- You have a function INSIDE another function
- The inner function USES a variable from the outer function
- The inner function is ACCESSIBLE after the outer function has returned
That's it. The inner function "closes over" the outer variables, keeping them alive.
The Five Closure Patterns Every Developer Must Know
Pattern 1: Data Privacy (The Module Pattern)
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private — no one can access directly
return {
deposit(amount) {
if (amount <= 0) throw new Error('Deposit must be positive');
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error('Insufficient funds');
balance -= amount;
return balance;
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(1000);
account.deposit(500); // 1500
account.withdraw(200); // 1300
account.getBalance(); // 1300
account.balance; // undefined — truly private!
Pattern 2: Function Factory
function createTaxCalculator(taxRate) {
return function(amount) {
return amount + (amount * taxRate);
};
}
const calcGST = createTaxCalculator(0.18); // India GST 18%
const calcVAT = createTaxCalculator(0.20); // UK VAT 20%
const calcSalesTax = createTaxCalculator(0.0725); // CA sales tax
calcGST(1000); // 1180
calcVAT(1000); // 1200
calcSalesTax(1000); // 1072.5
Pattern 3: Memoization / Caching
function createExpensiveProcessor() {
const cache = new Map();
return function process(input) {
if (cache.has(input)) {
console.log('Cache hit');
return cache.get(input);
}
console.log('Computing...');
// Simulate expensive operation
const result = input.split('').reverse().join('').toUpperCase();
cache.set(input, result);
return result;
};
}
const process = createExpensiveProcessor();
process('hello'); // Computing... → 'OLLEH'
process('hello'); // Cache hit → 'OLLEH' (instant)
process('world'); // Computing... → 'DLROW'
Pattern 4: Event Handler State
function createToggleButton(element) {
let isOn = false; // State persists across clicks via closure
element.addEventListener('click', () => {
isOn = !isOn;
element.textContent = isOn ? 'ON' : 'OFF';
element.style.backgroundColor = isOn ? '#4CAF50' : '#f44336';
});
}
Pattern 5: Partial Application
function createLogger(prefix) {
return function(message) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${prefix}] ${message}`);
};
}
const dbLog = createLogger('DATABASE');
const apiLog = createLogger('API');
const authLog = createLogger('AUTH');
dbLog('Connection established'); // [2026-08-04...] [DATABASE] Connection established
apiLog('Request received'); // [2026-08-04...] [API] Request received
authLog('User logged in'); // [2026-08-04...] [AUTH] User logged in
The Classic Closure Trap (and 3 Fixes)
// THE BUG
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 5, 5, 5, 5, 5 — because var i is shared across all iterations
// FIX 1: let (creates new binding per iteration)
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
// FIX 2: IIFE (creates new scope per iteration)
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// FIX 3: setTimeout's third argument (passed to the callback)
for (var i = 0; i < 5; i++) {
setTimeout((j) => console.log(j), 100, i);
}
Chapter 12: this — Every Single Rule, Ranked by Priority
When multiple rules could apply, the one higher on this list wins:
Rule 1 (Highest Priority): new Keyword
function Person(name) {
// this = brand new empty object
this.name = name;
// return this (implicit)
}
const alice = new Person('Alice');
// this = { name: 'Alice' }
Rule 2: Explicit Binding (call, apply, bind)
function greet() {
return `Hello, ${this.name}`;
}
const user = { name: 'Bob' };
greet.call(user); // 'Hello, Bob' — this = user
greet.apply(user); // 'Hello, Bob' — this = user
const bound = greet.bind(user);
bound(); // 'Hello, Bob' — this = user (permanently)
Rule 3: Implicit Binding (Dot Notation)
const obj = {
name: 'Charlie',
greet() {
return `Hello, ${this.name}`;
}
};
obj.greet(); // 'Hello, Charlie' — this = object before the dot
The Implicit Binding Trap (Lost this):
const obj = {
name: 'Charlie',
greet() { return `Hello, ${this.name}`; }
};
const fn = obj.greet; // Extracting the method — loses the object!
fn(); // 'Hello, undefined' — this is now global/undefined
// This happens ALL THE TIME with callbacks:
setTimeout(obj.greet, 100); // 'Hello, undefined'
// FIX: bind, or use an arrow function wrapper
setTimeout(() => obj.greet(), 100); // 'Hello, Charlie'
setTimeout(obj.greet.bind(obj), 100); // 'Hello, Charlie'
Rule 4 (Lowest Priority): Default Binding
function standalone() {
return this;
}
standalone(); // window (browser) or global (Node.js) — in sloppy mode
// undefined — in strict mode ('use strict')
Arrow Functions: NOT a Rule — A Different System
Arrow functions don't have their own this. They capture this from the enclosing scope at creation time. No rule can override it:
const obj = {
name: 'Dave',
// arrow function captures `this` from where it's written
// which is the global scope (or module scope)
greet: () => `Hello, ${this.name}`,
};
obj.greet(); // 'Hello, undefined' — this is NOT obj!
// Arrow functions shine inside methods:
const obj2 = {
name: 'Eve',
greetDelayed() {
// this = obj2 (regular method call)
setTimeout(() => {
// Arrow inherits this from greetDelayed — it's obj2!
console.log(`Hello, ${this.name}`);
}, 100);
},
};
obj2.greetDelayed(); // 'Hello, Eve' ✓
Chapter 13: call, apply, bind — Build Them Yourself
Understanding these deeply means building them from scratch.
Implement call
Function.prototype.myCall = function(context, ...args) {
// If context is null/undefined, default to globalThis
context = context ?? globalThis;
// Wrap primitive values
context = Object(context);
// Create a unique key so we don't overwrite existing properties
const uniqueKey = Symbol('fn');
context[uniqueKey] = this; // 'this' here is the function being called
const result = context[uniqueKey](...args);
delete context[uniqueKey]; // Clean up
return result;
};
Implement apply
Function.prototype.myApply = function(context, args = []) {
context = context ?? globalThis;
context = Object(context);
const uniqueKey = Symbol('fn');
context[uniqueKey] = this;
const result = context[uniqueKey](...args);
delete context[uniqueKey];
return result;
};
Implement bind
Function.prototype.myBind = function(context, ...boundArgs) {
const fn = this;
return function boundFn(...callArgs) {
// Support use as constructor with 'new'
if (new.target) {
return new fn(...boundArgs, ...callArgs);
}
return fn.apply(context, [...boundArgs, ...callArgs]);
};
};
Chapter 14: Higher-Order Functions & Functional Patterns
Build map, filter, reduce From Scratch
// MAP — transform every element
Array.prototype.myMap = function(callback) {
const result = [];
for (let i = 0; i < this.length; i++) {
if (i in this) { // Handle sparse arrays
result.push(callback(this[i], i, this));
}
}
return result;
};
// FILTER — keep elements that pass the test
Array.prototype.myFilter = function(callback) {
const result = [];
for (let i = 0; i < this.length; i++) {
if (i in this && callback(this[i], i, this)) {
result.push(this[i]);
}
}
return result;
};
// REDUCE — accumulate into a single value
Array.prototype.myReduce = function(callback, initialValue) {
let accumulator;
let startIndex;
if (arguments.length >= 2) {
accumulator = initialValue;
startIndex = 0;
} else {
if (this.length === 0) throw new TypeError('Reduce of empty array with no initial value');
accumulator = this[0];
startIndex = 1;
}
for (let i = startIndex; i < this.length; i++) {
if (i in this) {
accumulator = callback(accumulator, this[i], i, this);
}
}
return accumulator;
};
Compose and Pipe — Functional Architecture
// Compose: right to left (mathematical tradition)
const compose = (...fns) =>
(value) => fns.reduceRight((acc, fn) => fn(acc), value);
// Pipe: left to right (readable order)
const pipe = (...fns) =>
(value) => fns.reduce((acc, fn) => fn(acc), value);
// Usage — data processing pipeline
const processUser = pipe(
(user) => ({ ...user, name: user.name.trim() }),
(user) => ({ ...user, email: user.email.toLowerCase() }),
(user) => ({ ...user, age: Number(user.age) }),
(user) => {
if (user.age < 0 || user.age > 150) throw new Error('Invalid age');
return user;
},
);
processUser({ name: ' Alice ', email: 'ALICE@Example.COM', age: '30' });
// { name: 'Alice', email: 'alice@example.com', age: 30 }
PART IV — OBJECTS & PROTOTYPES
Chapter 15: The Prototype Chain — What class Is Hiding
The Chain Visualized
const dog = { bark() { return 'Woof!'; } };
// When you call dog.bark():
// 1. Engine checks: does dog have 'bark'? YES → call it
// When you call dog.toString():
// 1. Engine checks: does dog have 'toString'? NO
// 2. Follow dog.__proto__ → Object.prototype
// 3. Does Object.prototype have 'toString'? YES → call it
// When you call dog.fly():
// 1. Engine checks: does dog have 'fly'? NO
// 2. Follow dog.__proto__ → Object.prototype — NO
// 3. Follow Object.prototype.__proto__ → null (end of chain)
// 4. Return undefined
What class Actually Creates
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal {
bark() {
return `${this.name} says Woof!`;
}
}
// Under the hood, this creates:
// Dog.prototype = { bark: [Function], __proto__: Animal.prototype }
// Animal.prototype = { speak: [Function], __proto__: Object.prototype }
const rex = new Dog('Rex');
// rex = { name: 'Rex', __proto__: Dog.prototype }
// The prototype chain:
// rex → Dog.prototype → Animal.prototype → Object.prototype → null
Object.create — The Raw Prototype Tool
const vehicleProto = {
start() { return `${this.model} starting...`; },
stop() { return `${this.model} stopping...`; },
};
const car = Object.create(vehicleProto);
car.model = 'Tesla Model 3';
car.start(); // 'Tesla Model 3 starting...'
// car's prototype chain:
// car → vehicleProto → Object.prototype → null
Chapter 16: Property Descriptors
Every property has hidden attributes that control its behavior:
const user = { name: 'Alice' };
console.log(Object.getOwnPropertyDescriptor(user, 'name'));
// {
// value: 'Alice',
// writable: true, ← can the value change?
// enumerable: true, ← does it show in for...in loops?
// configurable: true ← can this descriptor be changed?
// }
// Make a property read-only
Object.defineProperty(user, 'id', {
value: 12345,
writable: false, // Can't change the value
enumerable: true, // Shows in Object.keys()
configurable: false, // Can't delete or reconfigure
});
user.id = 99999; // Silently fails (or throws in strict mode)
console.log(user.id); // 12345
Getters and Setters — Computed Properties
const temperature = {
_celsius: 0,
get fahrenheit() {
return this._celsius * 9/5 + 32;
},
set fahrenheit(value) {
this._celsius = (value - 32) * 5/9;
},
get celsius() {
return this._celsius;
},
set celsius(value) {
if (value < -273.15) throw new Error('Below absolute zero');
this._celsius = value;
},
};
temperature.celsius = 100;
console.log(temperature.fahrenheit); // 212
temperature.fahrenheit = 32;
console.log(temperature.celsius); // 0
Chapter 17: Composition vs Inheritance
Why "Favor Composition Over Inheritance"
Inheritance creates rigid hierarchies. What if you need a FlyingSwimmingRobot?
// INHERITANCE: The Diamond Problem
class Animal { eat() {} }
class Flyer extends Animal { fly() {} }
class Swimmer extends Animal { swim() {} }
// class FlyingSwimmer extends Flyer, Swimmer {} ← IMPOSSIBLE in JS
// COMPOSITION: Mix and match
const canFly = (state) => ({
fly: () => `${state.name} is flying at ${state.altitude}ft`,
});
const canSwim = (state) => ({
swim: () => `${state.name} is swimming at ${state.depth}m`,
});
const canWalk = (state) => ({
walk: () => `${state.name} is walking`,
});
function createDuck(name) {
const state = { name, altitude: 0, depth: 0 };
return {
...canFly(state),
...canSwim(state),
...canWalk(state),
quack: () => `${name}: Quack!`,
};
}
const duck = createDuck('Donald');
duck.fly(); // 'Donald is flying at 0ft'
duck.swim(); // 'Donald is swimming at 0m'
duck.quack(); // 'Donald: Quack!'
Chapter 18: Proxy & Reflect — Metaprogramming
Proxy lets you intercept and customize fundamental operations on objects.
Validation Proxy
function createValidatedObject(schema) {
return new Proxy({}, {
set(target, property, value) {
if (property in schema) {
const validator = schema[property];
if (!validator(value)) {
throw new TypeError(
`Invalid value for "${property}": ${JSON.stringify(value)}`
);
}
}
target[property] = value;
return true;
},
});
}
const user = createValidatedObject({
name: (v) => typeof v === 'string' && v.length > 0,
age: (v) => typeof v === 'number' && v >= 0 && v <= 150,
email: (v) => typeof v === 'string' && v.includes('@'),
});
user.name = 'Alice'; // ✓
user.age = 30; // ✓
user.email = 'a@b.com'; // ✓
user.age = -5; // TypeError: Invalid value for "age"
user.email = 'not-an-email'; // TypeError: Invalid value for "email"
Negative Array Indexing (Like Python)
function createSmartArray(...items) {
return new Proxy(items, {
get(target, prop) {
const index = Number(prop);
if (Number.isInteger(index) && index < 0) {
return target[target.length + index];
}
return Reflect.get(target, prop);
},
});
}
const arr = createSmartArray('a', 'b', 'c', 'd', 'e');
arr[-1]; // 'e'
arr[-2]; // 'd'
arr[0]; // 'a'
Observable Object (Change Detection)
function createObservable(target, onChange) {
return new Proxy(target, {
set(obj, prop, value) {
const oldValue = obj[prop];
obj[prop] = value;
onChange({ property: prop, oldValue, newValue: value });
return true;
},
deleteProperty(obj, prop) {
const oldValue = obj[prop];
delete obj[prop];
onChange({ property: prop, oldValue, newValue: undefined, deleted: true });
return true;
},
});
}
const state = createObservable({ count: 0, name: 'App' }, (change) => {
console.log(`${change.property}: ${change.oldValue} → ${change.newValue}`);
});
state.count = 1; // count: 0 → 1
state.name = 'MyApp'; // name: App → MyApp
PART V — ASYNC MASTERY
Chapter 19: Promises — Build One From Scratch
A Simplified Promise Implementation
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.callbacks = [];
const resolve = (value) => {
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.value = value;
this.callbacks.forEach((cb) => cb.onFulfilled(value));
};
const reject = (reason) => {
if (this.state !== 'pending') return;
this.state = 'rejected';
this.value = reason;
this.callbacks.forEach((cb) => cb.onRejected(reason));
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const handle = (callback, fallback) => (value) => {
try {
if (typeof callback === 'function') {
const result = callback(value);
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} else {
fallback(value);
}
} catch (err) {
reject(err);
}
};
if (this.state === 'fulfilled') {
queueMicrotask(() => handle(onFulfilled, resolve)(this.value));
} else if (this.state === 'rejected') {
queueMicrotask(() => handle(onRejected, reject)(this.value));
} else {
this.callbacks.push({
onFulfilled: handle(onFulfilled, resolve),
onRejected: handle(onRejected, reject),
});
}
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
static resolve(value) {
return new MyPromise((resolve) => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
}
Chapter 20: async/await — The Traps Nobody Warns You About
Trap 1: Sequential When You Mean Parallel
// SLOW — each awaits waits for the previous one (3 seconds total)
async function fetchAllSlow() {
const users = await fetchUsers(); // 1 second
const products = await fetchProducts(); // 1 second (starts AFTER users)
const orders = await fetchOrders(); // 1 second (starts AFTER products)
return { users, products, orders };
}
// FAST — all three run simultaneously (1 second total)
async function fetchAllFast() {
const [users, products, orders] = await Promise.all([
fetchUsers(),
fetchProducts(),
fetchOrders(),
]);
return { users, products, orders };
}
Trap 2: forEach Doesn't Wait for Async
// BROKEN — all requests fire simultaneously, forEach doesn't await
const ids = [1, 2, 3, 4, 5];
ids.forEach(async (id) => {
const data = await fetchData(id);
console.log(data); // Prints in random order!
});
console.log('Done'); // Prints BEFORE any data!
// FIX — for...of respects await
for (const id of ids) {
const data = await fetchData(id);
console.log(data); // Sequential, ordered
}
console.log('Done'); // Prints after ALL data
// FIX (parallel) — use Promise.all with map
const results = await Promise.all(ids.map((id) => fetchData(id)));
console.log('Done'); // Prints after ALL data (but they ran in parallel)
Trap 3: Missing Error Handling
// DANGEROUS — unhandled rejection crashes Node.js
async function riskyFunction() {
const data = await fetch('/api/data');
return data.json();
}
// SAFE — always wrap async logic
async function safeFunction() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('Fetch failed:', error.message);
return null; // or rethrow, or return a default
}
}
Trap 4: await Inside a Return
// These are DIFFERENT:
async function example1() {
return await fetchData(); // try/catch CAN catch errors from fetchData
}
async function example2() {
return fetchData(); // try/catch CANNOT catch errors — the promise is returned unwrapped
}
// This matters when you have a try/catch:
async function safe() {
try {
return await fetchData(); // Error caught ✓
} catch (err) {
console.error(err);
}
}
async function unsafe() {
try {
return fetchData(); // Error NOT caught — it escapes!
} catch (err) {
console.error(err); // Never runs
}
}
Chapter 21: Generators & Iterators — The Protocol
The Iterator Protocol
Any object with a next() method that returns { value, done } is an iterator:
function createRangeIterator(start, end) {
let current = start;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
};
}
const iter = createRangeIterator(1, 3);
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 2, done: false }
iter.next(); // { value: 3, done: false }
iter.next(); // { value: undefined, done: true }
Generators — Iterator Factories
Generators make iterators easy to write. The yield keyword pauses the function:
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i; // Pause here, return i, resume when .next() is called
}
}
// Use in for...of
for (const num of range(1, 5)) {
console.log(num); // 1, 2, 3, 4, 5
}
// Spread into array
const nums = [...range(1, 5)]; // [1, 2, 3, 4, 5]
Infinite Sequences
function* fibonacci() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Take the first 10 fibonacci numbers
function take(gen, n) {
const result = [];
for (const value of gen) {
result.push(value);
if (result.length >= n) break;
}
return result;
}
take(fibonacci(), 10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Async Generators — Streaming Data
async function* fetchPages(url) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${url}?page=${page}`);
const data = await response.json();
yield data.results;
hasMore = data.hasNextPage;
page += 1;
}
}
// Process pages as they arrive
for await (const pageResults of fetchPages('/api/users')) {
pageResults.forEach((user) => renderUser(user));
}
Chapter 22: Advanced Async Patterns
Pattern 1: Retry with Exponential Backoff
async function withRetry(fn, maxRetries = 3, baseDelay = 1000) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = baseDelay * Math.pow(2, attempt);
const jitter = delay * Math.random() * 0.1;
console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
}
}
}
// Usage
const data = await withRetry(() => fetch('/api/unstable-endpoint'), 5, 500);
Pattern 2: Cancellable Async Operations
function createCancellableRequest(url) {
const controller = new AbortController();
const promise = fetch(url, { signal: controller.signal })
.then((res) => res.json());
return {
promise,
cancel: () => controller.abort(),
};
}
// Usage — cancel previous search when user types again
let currentRequest = null;
function onSearchInput(query) {
if (currentRequest) currentRequest.cancel();
currentRequest = createCancellableRequest(`/api/search?q=${query}`);
currentRequest.promise
.then(renderResults)
.catch((err) => {
if (err.name !== 'AbortError') console.error(err);
});
}
Pattern 3: Concurrency Limiter
async function asyncPool(limit, items, fn) {
const results = [];
const executing = new Set();
for (const [index, item] of items.entries()) {
const promise = fn(item, index).then((result) => {
executing.delete(promise);
return result;
});
results.push(promise);
executing.add(promise);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// Process 100 URLs, but only 5 at a time
const urls = Array.from({ length: 100 }, (_, i) => `/api/page/${i}`);
const data = await asyncPool(5, urls, (url) => fetch(url).then((r) => r.json()));
PART VI — ADVANCED WEAPONS
Chapter 23: Symbols
Symbols are unique, immutable identifiers. Their primary use is adding properties to objects without risking name collisions.
// Every Symbol is unique
const id1 = Symbol('id');
const id2 = Symbol('id');
id1 === id2; // false — even with the same description
// Use as object keys
const user = {
name: 'Alice',
[id1]: 12345, // Hidden from normal iteration
};
Object.keys(user); // ['name'] — Symbol keys are invisible
Object.getOwnPropertySymbols(user); // [Symbol(id)] — only way to find them
Well-Known Symbols — Customizing Language Behavior
// Symbol.iterator — make any object iterable
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
return current <= end
? { value: current++, done: false }
: { done: true };
},
};
}
}
for (const n of new Range(1, 5)) console.log(n); // 1, 2, 3, 4, 5
[...new Range(3, 7)]; // [3, 4, 5, 6, 7]
// Symbol.toPrimitive — control type coercion
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.amount;
if (hint === 'string') return `${this.amount} ${this.currency}`;
return this.amount; // default
}
}
const price = new Money(42.5, 'USD');
+price; // 42.5
`${price}`; // '42.5 USD'
price + 10; // 52.5
Chapter 24: WeakMap, WeakSet, WeakRef
WeakMap — The Memory-Safe Cache
WeakMap keys are held weakly — if nothing else references the key, both the key and value get garbage collected:
// Associating private data with DOM elements without memory leaks
const elementData = new WeakMap();
function trackElement(element) {
elementData.set(element, {
clickCount: 0,
createdAt: Date.now(),
});
}
function handleClick(element) {
const data = elementData.get(element);
data.clickCount += 1;
}
// When the DOM element is removed from the page and no JS references remain,
// the WeakMap entry is automatically garbage collected. No cleanup needed!
WeakRef — When You Need a "Maybe" Reference
class ImageCache {
#cache = new Map();
get(url) {
const ref = this.#cache.get(url);
if (ref) {
const image = ref.deref(); // Returns the object, or undefined if GC'd
if (image) return image;
}
return null;
}
set(url, image) {
this.#cache.set(url, new WeakRef(image));
}
}
// Images are cached but can be garbage collected if memory is tight
Chapter 25: Tagged Template Literals
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i] !== undefined
? `<mark>${values[i]}</mark>`
: '';
return result + str + value;
}, '');
}
const name = 'Alice';
const role = 'Admin';
highlight`User ${name} has role ${role}`;
// 'User <mark>Alice</mark> has role <mark>Admin</mark>'
// Real use: SQL injection prevention
function sql(strings, ...values) {
const escaped = values.map((v) =>
typeof v === 'string' ? v.replace(/'/g, "''") : v
);
return strings.reduce((q, str, i) =>
q + str + (escaped[i] ?? ''), ''
);
}
PART VII — THE BROWSER
Chapter 28: Event Bubbling, Capturing & Delegation
The Three Phases
When you click a button inside a div inside the body:
CAPTURING (top down): window → document → body → div → button
TARGET: button (the element you actually clicked)
BUBBLING (bottom up): button → div → body → document → window
// By default, listeners fire during BUBBLING
parent.addEventListener('click', handler);
// To listen during CAPTURING, pass true as third argument
parent.addEventListener('click', handler, true);
// To stop the event from going further:
element.addEventListener('click', (e) => {
e.stopPropagation(); // Stops bubbling/capturing
});
Event Delegation — The Performance Pattern
Instead of 1000 listeners on 1000 list items, put ONE listener on the parent:
// BAD — 1000 event listeners
document.querySelectorAll('.item').forEach((item) => {
item.addEventListener('click', handleItemClick);
});
// GOOD — 1 event listener, handles all items (even future ones)
document.getElementById('list').addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (!item) return; // Click wasn't on an item
const itemId = item.dataset.id;
handleItemClick(itemId);
});
// This even works for items added AFTER the listener was created!
Chapter 29: Critical Rendering Path
When a browser loads a page:
- HTML → DOM (parse HTML into a tree of nodes)
- CSS → CSSOM (parse CSS into a style tree)
- DOM + CSSOM → Render Tree (combine — only visible elements)
- Layout (calculate positions and sizes)
- Paint (draw pixels)
- Composite (layer management for transforms, opacity)
What blocks rendering:
- CSS is render-blocking (the browser won't paint until CSSOM is built)
- JavaScript is parser-blocking (the browser stops parsing HTML when it hits a script tag)
- Solution:
<script defer>(parse HTML first, run JS after) or<script async>(download in parallel, run when ready)
Chapter 30: Web Storage
// localStorage — persists forever (until manually cleared)
localStorage.setItem('theme', 'dark');
localStorage.getItem('theme'); // 'dark'
localStorage.removeItem('theme');
// sessionStorage — persists until the tab is closed
sessionStorage.setItem('tempData', JSON.stringify({ x: 1 }));
// Cookies — sent with every HTTP request (use for auth tokens)
document.cookie = 'session=abc123; max-age=3600; path=/; secure';
// IndexedDB — for large, structured data (async API)
// Like a real database in the browser
Storage limits: localStorage/sessionStorage ~5-10MB. IndexedDB varies but typically 50MB+ (browser asks for permission beyond that). Cookies: 4KB per cookie.
PART VIII — PATTERNS & ARCHITECTURE
Chapter 33: Design Patterns in JavaScript
Singleton — One Instance Only
class Database {
static #instance = null;
constructor(connectionString) {
if (Database.#instance) {
return Database.#instance;
}
this.connectionString = connectionString;
this.connected = false;
Database.#instance = this;
}
connect() {
this.connected = true;
return `Connected to ${this.connectionString}`;
}
}
const db1 = new Database('mongodb://localhost');
const db2 = new Database('postgres://localhost'); // Returns same instance!
db1 === db2; // true
Observer / PubSub
class EventEmitter {
#events = new Map();
on(event, callback) {
if (!this.#events.has(event)) {
this.#events.set(event, []);
}
this.#events.get(event).push(callback);
// Return unsubscribe function
return () => this.off(event, callback);
}
off(event, callback) {
const handlers = this.#events.get(event);
if (handlers) {
this.#events.set(event, handlers.filter((h) => h !== callback));
}
}
emit(event, ...args) {
const handlers = this.#events.get(event) || [];
handlers.forEach((handler) => handler(...args));
}
once(event, callback) {
const unsubscribe = this.on(event, (...args) => {
callback(...args);
unsubscribe();
});
}
}
// Usage
const store = new EventEmitter();
const unsub = store.on('userLogin', (user) => {
console.log(`${user.name} logged in`);
});
store.emit('userLogin', { name: 'Alice' }); // 'Alice logged in'
unsub(); // Clean up
Strategy Pattern
const pricingStrategies = {
regular: (price) => price,
premium: (price) => price * 0.9, // 10% discount
vip: (price) => price * 0.8, // 20% discount
employee: (price) => price * 0.5, // 50% discount
};
function calculatePrice(basePrice, customerType) {
const strategy = pricingStrategies[customerType] || pricingStrategies.regular;
return strategy(basePrice);
}
calculatePrice(100, 'vip'); // 80
calculatePrice(100, 'employee'); // 50
calculatePrice(100, 'regular'); // 100
Chapter 34: Error Handling — A Production Strategy
Custom Error Classes
class AppError extends Error {
constructor(message, statusCode, code) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.code = code;
this.isOperational = true; // Distinguishes expected from unexpected errors
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} with id ${id} not found`, 404, 'NOT_FOUND');
}
}
class ValidationError extends AppError {
constructor(field, message) {
super(`Validation failed: ${field} — ${message}`, 400, 'VALIDATION');
this.field = field;
}
}
// Usage
function getUser(id) {
const user = database.find(id);
if (!user) throw new NotFoundError('User', id);
return user;
}
Global Error Boundaries
// Browser — catch all unhandled errors
window.addEventListener('error', (event) => {
logToService({
message: event.message,
filename: event.filename,
line: event.lineno,
stack: event.error?.stack,
});
});
// Catch unhandled Promise rejections
window.addEventListener('unhandledrejection', (event) => {
logToService({
message: event.reason?.message || 'Unknown promise rejection',
stack: event.reason?.stack,
});
event.preventDefault(); // Prevents console error
});
Chapter 35: Security
XSS (Cross-Site Scripting)
// VULNERABLE — inserting user input as HTML
element.innerHTML = userInput;
// If userInput is '<script>stealCookies()</script>' — game over
// SAFE — use textContent (escapes HTML)
element.textContent = userInput;
// SAFE — sanitize before inserting HTML
function escapeHTML(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
Prototype Pollution
// DANGEROUS — merging user-controlled objects
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Attacker sends: { "__proto__": { "isAdmin": true } }
merge({}, JSON.parse(userInput));
// Now EVERY object in the application has isAdmin === true!
// FIX — block dangerous keys
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue; // Skip dangerous keys
}
// ... rest of merge logic
}
}
PART IX — PERFORMANCE
Chapter 36: Memory Leaks — Detection & Prevention
// Quick detection technique: take heap snapshots
// 1. Open Chrome DevTools → Memory tab
// 2. Take a snapshot
// 3. Perform the action you suspect leaks
// 4. Take another snapshot
// 5. Compare — look for objects that shouldn't be growing
// Common React leak — missing cleanup
function useInterval(callback, delay) {
useEffect(() => {
const id = setInterval(callback, delay);
return () => clearInterval(id); // THIS LINE prevents the leak
}, [callback, delay]);
}
Chapter 37: Debounce, Throttle, and rAF
// DEBOUNCE — wait until action stops for N ms
// Use for: search input, window resize, form autosave
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// THROTTLE — run at most once every N ms
// Use for: scroll events, mousemove, API rate limiting
function throttle(fn, limit) {
let waiting = false;
return function(...args) {
if (!waiting) {
fn.apply(this, args);
waiting = true;
setTimeout(() => { waiting = false; }, limit);
}
};
}
// requestAnimationFrame throttle — perfectly synced with the screen
// Use for: animations, scroll-linked effects
function rafThrottle(fn) {
let ticking = false;
return function(...args) {
if (!ticking) {
requestAnimationFrame(() => {
fn.apply(this, args);
ticking = false;
});
ticking = true;
}
};
}
PART X — 50 CODING CHALLENGES
🟢 Foundation (Junior)
Challenge 1: isPalindrome
// Check if a string is a palindrome (reads same forwards and backwards)
function isPalindrome(str) {
const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, '');
return cleaned === cleaned.split('').reverse().join('');
}
isPalindrome('A man, a plan, a canal: Panama'); // true
isPalindrome('hello'); // false
Challenge 2: FizzBuzz (the "Can You Code?" test)
function fizzBuzz(n) {
return Array.from({ length: n }, (_, i) => {
const num = i + 1;
if (num % 15 === 0) return 'FizzBuzz';
if (num % 3 === 0) return 'Fizz';
if (num % 5 === 0) return 'Buzz';
return num;
});
}
Challenge 3: Reverse a string (without .reverse())
function reverseString(str) {
let result = '';
for (let i = str.length - 1; i >= 0; i--) {
result += str[i];
}
return result;
}
// Or using reduce:
const reverse = (str) => [...str].reduce((rev, char) => char + rev, '');
Challenge 4: Count character occurrences
function charCount(str) {
return [...str.toLowerCase()].reduce((counts, char) => {
counts[char] = (counts[char] || 0) + 1;
return counts;
}, {});
}
charCount('hello'); // { h: 1, e: 1, l: 2, o: 1 }
Challenge 5: Find the largest number in each sub-array
function largestOfEach(arr) {
return arr.map((sub) => Math.max(...sub));
}
largestOfEach([[4, 5, 1], [13, 27, 18], [32, 35, 37]]); // [5, 27, 37]
Challenge 6: Title case a sentence
function titleCase(str) {
return str
.toLowerCase()
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
titleCase("i'm a little tea pot"); // "I'm A Little Tea Pot"
Challenge 7: Remove duplicates from array
// Method 1: Set
const unique = (arr) => [...new Set(arr)];
// Method 2: filter
const unique2 = (arr) => arr.filter((item, i) => arr.indexOf(item) === i);
// Method 3: reduce
const unique3 = (arr) =>
arr.reduce((acc, item) => (acc.includes(item) ? acc : [...acc, item]), []);
Challenge 8: Chunk an array
function chunk(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
}
chunk([1, 2, 3, 4, 5, 6, 7], 3); // [[1,2,3], [4,5,6], [7]]
Challenge 9: Flatten an array (one level)
const flatten = (arr) => arr.reduce((flat, item) =>
flat.concat(Array.isArray(item) ? item : [item]), []
);
flatten([1, [2, 3], [4, [5]]]); // [1, 2, 3, 4, [5]]
Challenge 10: Find the missing number
// Array of 1..n with one missing number
function findMissing(arr, n) {
const expectedSum = (n * (n + 1)) / 2;
const actualSum = arr.reduce((sum, num) => sum + num, 0);
return expectedSum - actualSum;
}
findMissing([1, 2, 4, 5, 6], 6); // 3
Challenge 11: Count vowels
const countVowels = (str) =>
(str.match(/[aeiou]/gi) || []).length;
Challenge 12: Truncate a string
function truncate(str, maxLength) {
return str.length > maxLength ? str.slice(0, maxLength - 3) + '...' : str;
}
Challenge 13: Sum all numbers in a range
function rangeSum(a, b) {
const [min, max] = [Math.min(a, b), Math.max(a, b)];
return ((max - min + 1) * (min + max)) / 2;
}
Challenge 14: Capitalize first letter of each word
const capitalize = (str) =>
str.replace(/\b\w/g, (char) => char.toUpperCase());
Challenge 15: Check if two strings are anagrams
function isAnagram(str1, str2) {
const normalize = (s) => s.toLowerCase().replace(/\s/g, '').split('').sort().join('');
return normalize(str1) === normalize(str2);
}
isAnagram('listen', 'silent'); // true
🟡 Intermediate (Mid-Level)
Challenge 16: Deep flatten (any depth)
function deepFlatten(arr) {
return arr.reduce(
(flat, item) =>
flat.concat(Array.isArray(item) ? deepFlatten(item) : item),
[]
);
}
deepFlatten([1, [2, [3, [4, [5]]]]]); // [1, 2, 3, 4, 5]
Challenge 17: Group by property
function groupBy(arr, key) {
return arr.reduce((groups, item) => {
const groupKey = typeof key === 'function' ? key(item) : item[key];
(groups[groupKey] = groups[groupKey] || []).push(item);
return groups;
}, {});
}
const people = [
{ name: 'Alice', dept: 'Eng' },
{ name: 'Bob', dept: 'Sales' },
{ name: 'Charlie', dept: 'Eng' },
];
groupBy(people, 'dept');
// { Eng: [{name: 'Alice'...}, {name: 'Charlie'...}], Sales: [{name: 'Bob'...}] }
Challenge 18: Deep equality check
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') {
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]));
}
return false;
}
Challenge 19: Implement debounce with leading/trailing options
function debounce(fn, delay, { leading = false, trailing = true } = {}) {
let timer = null;
let lastArgs = null;
return function(...args) {
const callNow = leading && timer === null;
lastArgs = args;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (trailing && lastArgs) {
fn.apply(this, lastArgs);
lastArgs = null;
}
}, delay);
if (callNow) {
fn.apply(this, args);
lastArgs = null;
}
};
}
Challenge 20: Implement Array.prototype.flat
function flat(arr, depth = 1) {
if (depth <= 0) return arr.slice();
return arr.reduce((acc, item) => {
if (Array.isArray(item)) {
acc.push(...flat(item, depth - 1));
} else {
acc.push(item);
}
return acc;
}, []);
}
flat([1, [2, [3, [4]]]], 2); // [1, 2, 3, [4]]
flat([1, [2, [3, [4]]]], Infinity); // [1, 2, 3, 4]
Challenge 21: Implement Object.assign
function objectAssign(target, ...sources) {
if (target == null) throw new TypeError('Cannot convert undefined or null to object');
const result = Object(target);
sources.forEach((source) => {
if (source != null) {
Object.keys(source).forEach((key) => {
result[key] = source[key];
});
// Also copy Symbol keys
Object.getOwnPropertySymbols(source).forEach((sym) => {
if (Object.prototype.propertyIsEnumerable.call(source, sym)) {
result[sym] = source[sym];
}
});
}
});
return result;
}
Challenge 22: Implement a simple EventEmitter
class EventEmitter {
constructor() {
this.events = {};
}
on(event, fn) {
(this.events[event] = this.events[event] || []).push(fn);
return this;
}
off(event, fn) {
this.events[event] = (this.events[event] || []).filter((f) => f !== fn);
return this;
}
emit(event, ...args) {
(this.events[event] || []).forEach((fn) => fn(...args));
return this;
}
once(event, fn) {
const wrapper = (...args) => {
fn(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
}
Challenge 23: Convert object to query string and back
function toQueryString(obj) {
return Object.entries(obj)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
}
function fromQueryString(qs) {
return Object.fromEntries(
qs.split('&').map((pair) => pair.split('=').map(decodeURIComponent))
);
}
toQueryString({ name: 'Alice', age: 30, city: 'New York' });
// 'name=Alice&age=30&city=New%20York'
Challenge 24: Implement Promise.allSettled
function allSettled(promises) {
return Promise.all(
promises.map((p) =>
Promise.resolve(p)
.then((value) => ({ status: 'fulfilled', value }))
.catch((reason) => ({ status: 'rejected', reason }))
)
);
}
Challenge 25: Implement JSON.stringify (basic version)
function jsonStringify(value) {
if (value === null) return 'null';
if (typeof value === 'boolean') return String(value);
if (typeof value === 'number') {
return Number.isFinite(value) ? String(value) : 'null';
}
if (typeof value === 'string') return `"${value}"`;
if (Array.isArray(value)) {
const items = value.map((item) => jsonStringify(item) ?? 'null');
return `[${items.join(',')}]`;
}
if (typeof value === 'object') {
const pairs = Object.keys(value)
.filter((key) => value[key] !== undefined && typeof value[key] !== 'function')
.map((key) => `"${key}":${jsonStringify(value[key])}`);
return `{${pairs.join(',')}}`;
}
return undefined;
}
Challenge 26: Implement a deep clone
function deepClone(obj, seen = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
if (seen.has(obj)) return seen.get(obj); // Handle circular references
const clone = Array.isArray(obj) ? [] : {};
seen.set(obj, clone);
for (const key of Reflect.ownKeys(obj)) {
clone[key] = deepClone(obj[key], seen);
}
return clone;
}
Challenge 27: LRU Cache
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map(); // Map preserves insertion order
}
get(key) {
if (!this.cache.has(key)) return -1;
// Move to end (most recently used)
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// Delete the least recently used (first item in Map)
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, value);
}
}
Challenge 28: Difference between two objects
function diff(obj1, obj2) {
const result = {};
const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
for (const key of allKeys) {
if (!(key in obj1)) {
result[key] = { type: 'added', value: obj2[key] };
} else if (!(key in obj2)) {
result[key] = { type: 'removed', value: obj1[key] };
} else if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') {
const nested = diff(obj1[key], obj2[key]);
if (Object.keys(nested).length > 0) {
result[key] = { type: 'nested', changes: nested };
}
} else if (obj1[key] !== obj2[key]) {
result[key] = { type: 'changed', from: obj1[key], to: obj2[key] };
}
}
return result;
}
Challenge 29: Implement setInterval using setTimeout
function mySetInterval(fn, delay) {
let cancelled = false;
function loop() {
if (!cancelled) {
fn();
setTimeout(loop, delay);
}
}
setTimeout(loop, delay);
return { cancel: () => { cancelled = true; } };
}
Challenge 30: Pipe / Compose
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x);
const processText = pipe(
(s) => s.trim(),
(s) => s.toLowerCase(),
(s) => s.replace(/\s+/g, '-'),
);
processText(' Hello World '); // 'hello-world'
Challenge 31: Curry function
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...more) => curried(...args, ...more);
};
}
const add = curry((a, b, c) => a + b + c);
add(1)(2)(3); // 6
add(1, 2)(3); // 6
add(1)(2, 3); // 6
Challenge 32: Throttle with leading and trailing
function throttle(fn, limit) {
let waiting = false;
let lastArgs = null;
return function(...args) {
if (!waiting) {
fn.apply(this, args);
waiting = true;
setTimeout(() => {
waiting = false;
if (lastArgs) {
fn.apply(this, lastArgs);
lastArgs = null;
}
}, limit);
} else {
lastArgs = args;
}
};
}
Challenge 33: getElementsByClassName (without the built-in)
function getElementsByClassName(root, className) {
const result = [];
function traverse(node) {
if (node.classList && node.classList.contains(className)) {
result.push(node);
}
for (const child of node.children) {
traverse(child);
}
}
traverse(root);
return result;
}
Challenge 34: Implement a simple Virtual DOM diff
function diff(oldNode, newNode) {
// Node was removed
if (!newNode) return { type: 'REMOVE' };
// Text nodes
if (typeof oldNode === 'string' || typeof newNode === 'string') {
if (oldNode !== newNode) return { type: 'TEXT', value: newNode };
return null;
}
// Different tag
if (oldNode.tag !== newNode.tag) return { type: 'REPLACE', value: newNode };
// Same tag — diff children and props
const propPatches = diffProps(oldNode.props, newNode.props);
const childPatches = diffChildren(oldNode.children, newNode.children);
return { type: 'UPDATE', propPatches, childPatches };
}
function diffProps(oldProps = {}, newProps = {}) {
const patches = [];
// Check new and changed props
for (const [key, value] of Object.entries(newProps)) {
if (oldProps[key] !== value) patches.push({ key, value });
}
// Check removed props
for (const key of Object.keys(oldProps)) {
if (!(key in newProps)) patches.push({ key, value: undefined });
}
return patches;
}
function diffChildren(oldChildren = [], newChildren = []) {
return newChildren.map((child, i) => diff(oldChildren[i], child));
}
Challenge 35: Implement Promise.race
function promiseRace(promises) {
return new Promise((resolve, reject) => {
for (const promise of promises) {
Promise.resolve(promise).then(resolve, reject);
}
});
}
🔴 Advanced (Senior / Staff)
Challenge 36: Implement async/await using generators
function asyncRunner(generatorFn) {
return function(...args) {
const gen = generatorFn(...args);
return new Promise((resolve, reject) => {
function step(nextFn) {
let result;
try {
result = nextFn();
} catch (err) {
return reject(err);
}
if (result.done) return resolve(result.value);
Promise.resolve(result.value).then(
(value) => step(() => gen.next(value)),
(err) => step(() => gen.throw(err))
);
}
step(() => gen.next());
});
};
}
// Usage — this behaves exactly like async/await
const fetchData = asyncRunner(function* () {
const response = yield fetch('/api/data');
const data = yield response.json();
return data;
});
Challenge 37: Implement a task scheduler with concurrency limit
class TaskScheduler {
constructor(concurrency) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
add(asyncFn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn: asyncFn, resolve, reject });
this.#run();
});
}
#run() {
while (this.running < this.concurrency && this.queue.length > 0) {
const { fn, resolve, reject } = this.queue.shift();
this.running += 1;
fn()
.then(resolve)
.catch(reject)
.finally(() => {
this.running -= 1;
this.#run();
});
}
}
}
// Usage — fetch 100 URLs, max 3 at a time
const scheduler = new TaskScheduler(3);
const urls = Array.from({ length: 100 }, (_, i) => `https://api.example.com/${i}`);
const results = await Promise.all(
urls.map((url) => scheduler.add(() => fetch(url).then((r) => r.json())))
);
Challenge 38: Implement Function.prototype.bind (spec-compliant)
Function.prototype.myBind = function(thisArg, ...boundArgs) {
if (typeof this !== 'function') {
throw new TypeError('Bind must be called on a function');
}
const targetFn = this;
const BoundFn = function(...callArgs) {
// If used as a constructor (with new), ignore thisArg
const context = this instanceof BoundFn ? this : thisArg;
return targetFn.apply(context, [...boundArgs, ...callArgs]);
};
// Maintain prototype chain for new
if (targetFn.prototype) {
BoundFn.prototype = Object.create(targetFn.prototype);
}
return BoundFn;
};
Challenge 39: Implement JSON.parse (basic)
function jsonParse(str) {
let i = 0;
function parseValue() {
skipWhitespace();
const char = str[i];
if (char === '"') return parseString();
if (char === '{') return parseObject();
if (char === '[') return parseArray();
if (char === 't') return parseLiteral('true', true);
if (char === 'f') return parseLiteral('false', false);
if (char === 'n') return parseLiteral('null', null);
return parseNumber();
}
function parseString() {
i++; // skip opening "
let result = '';
while (str[i] !== '"') {
if (str[i] === '\\') {
i++;
const escapes = { n: '\n', t: '\t', r: '\r', '"': '"', '\\': '\\' };
result += escapes[str[i]] || str[i];
} else {
result += str[i];
}
i++;
}
i++; // skip closing "
return result;
}
function parseNumber() {
const start = i;
if (str[i] === '-') i++;
while (i < str.length && /[\d.eE+\-]/.test(str[i])) i++;
return Number(str.slice(start, i));
}
function parseObject() {
i++; // skip {
const obj = {};
skipWhitespace();
if (str[i] === '}') { i++; return obj; }
while (true) {
skipWhitespace();
const key = parseString();
skipWhitespace();
i++; // skip :
obj[key] = parseValue();
skipWhitespace();
if (str[i] === '}') { i++; return obj; }
i++; // skip ,
}
}
function parseArray() {
i++; // skip [
const arr = [];
skipWhitespace();
if (str[i] === ']') { i++; return arr; }
while (true) {
arr.push(parseValue());
skipWhitespace();
if (str[i] === ']') { i++; return arr; }
i++; // skip ,
}
}
function parseLiteral(text, value) {
i += text.length;
return value;
}
function skipWhitespace() {
while (i < str.length && ' \t\n\r'.includes(str[i])) i++;
}
return parseValue();
}
Challenge 40: Implement a Reactive State System (like Vue/MobX)
function reactive(obj) {
const subscribers = new Map();
let activeEffect = null;
function track(key) {
if (activeEffect) {
if (!subscribers.has(key)) subscribers.set(key, new Set());
subscribers.get(key).add(activeEffect);
}
}
function trigger(key) {
const effects = subscribers.get(key);
if (effects) effects.forEach((effect) => effect());
}
const proxy = new Proxy(obj, {
get(target, key) {
track(key);
return target[key];
},
set(target, key, value) {
target[key] = value;
trigger(key);
return true;
},
});
function effect(fn) {
activeEffect = fn;
fn(); // Run once to collect dependencies
activeEffect = null;
}
return { state: proxy, effect };
}
// Usage
const { state, effect } = reactive({ count: 0, name: 'App' });
effect(() => {
console.log(`Count is: ${state.count}`);
});
state.count = 1; // Automatically logs: "Count is: 1"
state.count = 2; // Automatically logs: "Count is: 2"
Challenge 41: Implement a simple template engine
function template(str) {
return function(data) {
return str.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return data[key] !== undefined ? data[key] : match;
});
};
}
const render = template('Hello {{name}}, you are {{age}} years old!');
render({ name: 'Alice', age: 30 });
// 'Hello Alice, you are 30 years old!'
Challenge 42: Implement a dependency injection container
class Container {
#factories = new Map();
#singletons = new Map();
register(name, factory, singleton = false) {
this.#factories.set(name, { factory, singleton });
}
resolve(name) {
const entry = this.#factories.get(name);
if (!entry) throw new Error(`"${name}" is not registered`);
if (entry.singleton) {
if (!this.#singletons.has(name)) {
this.#singletons.set(name, entry.factory(this));
}
return this.#singletons.get(name);
}
return entry.factory(this);
}
}
// Usage
const container = new Container();
container.register('logger', () => ({ log: console.log }), true);
container.register('userService', (c) => ({
getUser: (id) => {
c.resolve('logger').log(`Fetching user ${id}`);
return { id, name: 'Alice' };
},
}));
const userService = container.resolve('userService');
userService.getUser(1); // logs "Fetching user 1", returns { id: 1, name: 'Alice' }
Challenge 43: Implement a middleware pipeline (like Express)
function createApp() {
const middlewares = [];
function use(fn) {
middlewares.push(fn);
}
function handle(req, res) {
let index = 0;
function next(err) {
if (index >= middlewares.length) return;
const middleware = middlewares[index++];
try {
if (err) {
// Error-handling middleware has 4 params
if (middleware.length === 4) {
middleware(err, req, res, next);
} else {
next(err); // Skip non-error middleware
}
} else {
middleware(req, res, next);
}
} catch (e) {
next(e);
}
}
next();
}
return { use, handle };
}
Challenge 44: Implement a Promise-based sleep + timeout utility
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function withTimeout(promise, ms, message = 'Operation timed out') {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(message)), ms)
);
return Promise.race([promise, timeout]);
}
// Usage
const data = await withTimeout(fetch('/api/slow'), 5000, 'API call timed out');
Challenge 45: Implement Object.is
function objectIs(a, b) {
// Handle +0 !== -0
if (a === 0 && b === 0) {
return 1 / a === 1 / b; // 1/+0 = Infinity, 1/-0 = -Infinity
}
// Handle NaN === NaN
if (a !== a && b !== b) return true; // Only NaN !== NaN
return a === b;
}
objectIs(+0, -0); // false (=== says true)
objectIs(NaN, NaN); // true (=== says false)
objectIs(42, 42); // true
Challenge 46: Implement a pub/sub with wildcard support
class WildcardEmitter {
constructor() {
this.handlers = new Map();
}
on(pattern, handler) {
if (!this.handlers.has(pattern)) {
this.handlers.set(pattern, []);
}
this.handlers.get(pattern).push(handler);
}
emit(event, ...args) {
this.handlers.forEach((handlers, pattern) => {
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
if (regex.test(event)) {
handlers.forEach((h) => h(...args));
}
});
}
}
const emitter = new WildcardEmitter();
emitter.on('user.*', (data) => console.log('User event:', data));
emitter.on('user.login', (data) => console.log('Login:', data));
emitter.emit('user.login', { name: 'Alice' });
// "User event: { name: 'Alice' }"
// "Login: { name: 'Alice' }"
Challenge 47: Implement a memoize with TTL (cache expiry)
function memoizeWithTTL(fn, ttl = 60000) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.value;
}
const result = fn.apply(this, args);
cache.set(key, { value: result, timestamp: Date.now() });
// Cleanup expired entries periodically
if (cache.size > 100) {
const now = Date.now();
for (const [k, v] of cache) {
if (now - v.timestamp >= ttl) cache.delete(k);
}
}
return result;
};
}
Challenge 48: Implement a simple state machine
function createStateMachine(config) {
let currentState = config.initial;
return {
get state() { return currentState; },
transition(event) {
const stateConfig = config.states[currentState];
const nextState = stateConfig?.on?.[event];
if (!nextState) {
throw new Error(`Invalid transition: ${currentState} + ${event}`);
}
const prevState = currentState;
currentState = nextState;
config.onTransition?.({ from: prevState, to: nextState, event });
return currentState;
},
};
}
// Usage — traffic light
const light = createStateMachine({
initial: 'red',
states: {
red: { on: { NEXT: 'green' } },
green: { on: { NEXT: 'yellow' } },
yellow: { on: { NEXT: 'red' } },
},
onTransition: ({ from, to }) => console.log(`${from} → ${to}`),
});
light.transition('NEXT'); // red → green
light.transition('NEXT'); // green → yellow
light.transition('NEXT'); // yellow → red
Challenge 49: Implement Array.prototype.reduce using recursion
function reduceRecursive(arr, callback, accumulator, index = 0) {
if (index >= arr.length) return accumulator;
const newAcc = arguments.length >= 3 || index > 0
? callback(accumulator, arr[index], index, arr)
: arr[index]; // No initial value: use first element
return reduceRecursive(arr, callback, newAcc, index + 1);
}
reduceRecursive([1, 2, 3, 4], (sum, n) => sum + n, 0); // 10
Challenge 50: Implement a lazy evaluation chain
class Lazy {
constructor(source) {
this.source = source;
this.operations = [];
}
static from(iterable) {
return new Lazy(iterable);
}
map(fn) {
const clone = new Lazy(this.source);
clone.operations = [...this.operations, { type: 'map', fn }];
return clone;
}
filter(fn) {
const clone = new Lazy(this.source);
clone.operations = [...this.operations, { type: 'filter', fn }];
return clone;
}
take(n) {
const clone = new Lazy(this.source);
clone.operations = [...this.operations, { type: 'take', n }];
return clone;
}
toArray() {
const result = [];
let taken = 0;
let takeLimit = Infinity;
// Find the take limit
for (const op of this.operations) {
if (op.type === 'take') takeLimit = op.n;
}
for (const item of this.source) {
let value = item;
let keep = true;
for (const op of this.operations) {
if (op.type === 'map') {
value = op.fn(value);
} else if (op.type === 'filter') {
if (!op.fn(value)) { keep = false; break; }
}
}
if (keep) {
result.push(value);
taken += 1;
if (taken >= takeLimit) break;
}
}
return result;
}
}
// Usage — processes only the minimum elements needed
const result = Lazy.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
.filter((n) => n % 2 === 0)
.map((n) => n * 10)
.take(3)
.toArray();
// [20, 40, 60] — only processed 6 elements, not all 10
BONUS: The Interview Mindset
How to Answer ANY JavaScript Question
Step 1 — Name It: "This question is about closures..."
Step 2 — Define It Simply: "A closure is when a function remembers variables from the scope where it was created."
Step 3 — Draw It or Code It: Write a 5-line example on the whiteboard.
Step 4 — Connect It: "You see this in React hooks — useState is literally a closure."
Step 5 — Show the Edge Case: "The classic trap is the for-loop with var, where all callbacks share the same variable."
The Questions YOU Should Ask the Interviewer
- "What's your team's approach to error handling?"
- "Do you use TypeScript, or any static analysis tools?"
- "What's the most interesting JavaScript bug your team has dealt with?"
- "How do you handle state management in your frontend?"
These show that you think about code quality, not just code output.
Quick Reference: What to Study by Interview Level
Junior: Types, scope, closures (basic), array methods, Promises, ES6 syntax, DOM basics.
Mid-Level: Event loop, prototype chain, this binding, async patterns, error handling, design patterns, performance basics.
Senior: Engine internals, memory management, Proxy/Reflect, generators, security, system design patterns, build tooling, mentoring ability.
Staff+: Architecture decisions, trade-off analysis, performance profiling, cross-team API design, ability to explain complex concepts simply.
This guide distills the principles from the Airbnb JavaScript Style Guide and extends them into the full depth of the language. The style guide tells you WHAT to write. This guide teaches you WHY — so that no question, no edge case, and no production bug can catch you off guard.
Keep building. Keep breaking things. Keep learning.