01-javascript-fundamentals.md

Module 01: JavaScript Fundamentals

Goal: Build an unshakable foundation in JavaScript. Everything else depends on this. Time: 3-4 days of focused study Prerequisites: None — we start from absolute zero


Table of Contents

  1. What is JavaScript?
  2. Running JavaScript
  3. Variables — var, let, const
  4. Data Types
  5. Type Coercion — JavaScript's Biggest Gotcha
  6. Operators
  7. Strings — Deep Dive
  8. Numbers — Deep Dive
  9. Control Flow
  10. Functions — The Heart of JavaScript
  11. Scope — Where Variables Live
  12. Hoisting — Why Order Doesn't Always Matter
  13. Closures — The Most Important Concept
  14. Objects — Deep Dive
  15. Arrays — Deep Dive
  16. The this Keyword
  17. Prototypes & The Prototype Chain
  18. Classes
  19. Error Handling
  20. ES6+ Features You Must Know
  21. Practice Problems
  22. Interview Questions

1. What is JavaScript?

JavaScript is a high-level, interpreted, dynamically-typed, single-threaded programming language. Let's break each word down:

  • High-level: You don't manage memory manually (unlike C/C++). The engine handles it.
  • Interpreted: Code is executed line by line (though modern engines like V8 actually compile it — more on this in Module 03).
  • Dynamically-typed: Variables don't have fixed types. A variable can hold a number, then a string, then an object.
  • Single-threaded: JavaScript runs one piece of code at a time (but it handles async operations cleverly — Module 02).

A Brief History (Interview Favorite)

  • 1995: Brendan Eich created JavaScript in 10 days at Netscape. It was originally called "Mocha," then "LiveScript," then "JavaScript" (marketing stunt to ride Java's popularity).
  • 1997: ECMAScript 1 (ES1) standardized by ECMA International.
  • 2009: Node.js created by Ryan Dahl — JavaScript could now run outside the browser.
  • 2015: ES6 (ES2015) — the massive update that modernized JavaScript (let/const, arrow functions, classes, promises, etc.)
  • 2015+: Yearly releases (ES2016, ES2017, ... ES2024)

JavaScript vs Other Languages

Python: x = 10 # Dynamic typing, interpreted Java: int x = 10; // Static typing, compiled JavaScript: let x = 10; // Dynamic typing, JIT compiled

Key difference from Java: JavaScript has first-class functions (functions are values — you can pass them around like numbers or strings). This is HUGE and we'll explore it deeply.


2. Running JavaScript

In the Browser Console

  1. Open Chrome → Press F12 → Go to "Console" tab
  2. Type console.log("Hello World") → Press Enter

With Node.js (What We'll Use)

# Create a file echo 'console.log("Hello from Node.js!")' > hello.js # Run it node hello.js # Output: Hello from Node.js!

The REPL (Read-Eval-Print-Loop)

# Start Node.js REPL node # Now you can type JavaScript directly > 2 + 2 4 > "hello".toUpperCase() 'HELLO' > .exit # to quit

3. Variables — var, let, const

The Three Ways to Declare Variables

var name = "Alice"; // Old way (ES5) — avoid in modern code let age = 25; // Modern way — use when value will change const PI = 3.14159; // Modern way — use when value won't change

Why var is Problematic

// Problem 1: var is function-scoped, not block-scoped if (true) { var x = 10; } console.log(x); // 10 — x leaked out of the if block! if (true) { let y = 10; } console.log(y); // ReferenceError: y is not defined — let stays in the block // Problem 2: var can be re-declared var name = "Alice"; var name = "Bob"; // No error — silently overwrites! let name2 = "Alice"; let name2 = "Bob"; // SyntaxError: Identifier 'name2' has already been declared // Problem 3: var hoists differently (see Hoisting section) console.log(a); // undefined (no error!) var a = 5; console.log(b); // ReferenceError: Cannot access 'b' before initialization let b = 5;

let vs const — When to Use Which

// Use const by DEFAULT. Only use let when you NEED to reassign. // const does NOT mean "immutable" — it means "cannot be reassigned" const user = { name: "Alice", age: 25 }; user.name = "Bob"; // ✅ This works! You changed the object, not the binding. user = { name: "Charlie" }; // ❌ TypeError: Assignment to constant variable. const numbers = [1, 2, 3]; numbers.push(4); // ✅ This works! Array is modified, not reassigned. numbers = [5, 6, 7]; // ❌ TypeError: Assignment to constant variable. // Use let when you need to reassign let count = 0; count = count + 1; // ✅ Need let because we're reassigning // Loop variables need let for (let i = 0; i < 10; i++) { // i changes each iteration }

🎯 Interview Tip

Q: What's the difference between var, let, and const? A: var is function-scoped and hoisted with undefined. let and const are block-scoped and hoisted but in a "temporal dead zone" (TDZ) until their declaration. const prevents reassignment but doesn't make objects immutable.


4. Data Types

JavaScript has 8 data types — 7 primitives and 1 non-primitive:

Primitive Types (Immutable, Stored by Value)

// 1. String let name = "Alice"; let greeting = 'Hello'; let template = `Hi ${name}`; // Template literal (ES6) // 2. Number (both integers and floats — no separate int/float) let age = 25; let price = 19.99; let infinity = Infinity; let notANumber = NaN; // "Not a Number" — but typeof NaN === "number" 🤯 // 3. BigInt (ES2020 — for numbers larger than 2^53 - 1) let bigNumber = 9007199254740991n; // Note the 'n' suffix let anotherBig = BigInt("12345678901234567890"); // 4. Boolean let isActive = true; let isDeleted = false; // 5. undefined — variable declared but not assigned let x; console.log(x); // undefined // 6. null — intentional absence of value let user = null; // "There is no user" // 7. Symbol (ES6 — unique identifier, used in advanced patterns) let id = Symbol("id"); let anotherId = Symbol("id"); console.log(id === anotherId); // false — every Symbol is unique

Non-Primitive Type (Mutable, Stored by Reference)

// 8. Object — everything that's not a primitive let person = { name: "Alice", age: 25 }; // Object literal let colors = ["red", "green", "blue"]; // Array (a type of object) let greet = function() { return "hi"; }; // Function (a type of object) let today = new Date(); // Date (a type of object) let pattern = /hello/gi; // RegExp (a type of object)

Stored by Value vs Stored by Reference — CRITICAL CONCEPT

// PRIMITIVES are copied by VALUE let a = 10; let b = a; // b gets a COPY of a's value b = 20; console.log(a); // 10 — a is unchanged. They're independent. // OBJECTS are copied by REFERENCE let obj1 = { name: "Alice" }; let obj2 = obj1; // obj2 points to the SAME object in memory obj2.name = "Bob"; console.log(obj1.name); // "Bob" — obj1 is affected because both point to same object! // This is why you need to clone objects: let obj3 = { ...obj1 }; // Shallow copy using spread let obj4 = JSON.parse(JSON.stringify(obj1)); // Deep copy (simple way) let obj5 = structuredClone(obj1); // Deep copy (modern way, Node 17+)

The typeof Operator

typeof "hello" // "string" typeof 42 // "number" typeof true // "boolean" typeof undefined // "undefined" typeof null // "object" ← THIS IS A BUG! It's been in JS since 1995. typeof {} // "object" typeof [] // "object" ← Arrays are objects! typeof function(){} // "function" typeof Symbol() // "symbol" typeof 42n // "bigint" // How to properly check for arrays: Array.isArray([1, 2, 3]); // true Array.isArray({}); // false // How to properly check for null: let val = null; val === null; // true (use strict equality)

Truthy and Falsy Values

Every value in JavaScript is either "truthy" or "falsy" when converted to boolean:

// FALSY values (there are exactly 8): false 0 -0 0n // BigInt zero "" // empty string null undefined NaN // EVERYTHING ELSE is truthy, including: true 42 "0" // non-empty string — even "0" is truthy! "false" // non-empty string — even "false" is truthy! [] // empty array is truthy! {} // empty object is truthy! function(){} // functions are truthy // This is used heavily in conditions: let username = ""; if (username) { console.log("Has username"); } else { console.log("No username"); // ← This runs because "" is falsy } // Common pattern: default values let name = userInput || "Anonymous"; // If userInput is falsy, use "Anonymous" // But there's a problem with || let count = 0; let result = count || 10; // result = 10, because 0 is falsy! // Use nullish coalescing (??) instead — only treats null/undefined as "missing" let result2 = count ?? 10; // result2 = 0, because 0 is not null/undefined

5. Type Coercion — JavaScript's Biggest Gotcha

JavaScript automatically converts types when you do operations with mixed types. This causes a LOT of bugs and is a top interview topic.

Implicit Coercion (Automatic)

// String + Number = String (concatenation wins) "5" + 3 // "53" (3 is converted to "3") "5" + true // "5true" "5" + null // "5null" "5" + undefined // "5undefined" // Number operations with strings (-, *, / convert to numbers) "5" - 3 // 2 (string "5" → number 5) "5" * 3 // 15 "6" / 2 // 3 "5" - true // 4 (true → 1) "5" - false // 5 (false → 0) // Comparison chaos 0 == "" // true (empty string → 0) 0 == "0" // true ("0" → 0) "" == "0" // false (both strings, different values) false == "0" // true (false → 0, "0" → 0) false == "" // true (both → 0) null == undefined // true (special rule) null == 0 // false (null only equals undefined) NaN == NaN // false (NaN is not equal to anything, not even itself!) // The famous WAT examples [] + [] // "" (both convert to empty string) [] + {} // "[object Object]" {} + [] // 0 (in browser console — {} is treated as empty block) true + true // 2 true + false // 1

Explicit Coercion (Manual — The Right Way)

// To Number Number("123") // 123 Number("123abc") // NaN Number(true) // 1 Number(false) // 0 Number(null) // 0 Number(undefined) // NaN parseInt("123abc") // 123 (parses until it hits non-number) parseFloat("3.14") // 3.14 +"42" // 42 (unary plus — shorthand for Number()) // To String String(123) // "123" String(true) // "true" String(null) // "null" String(undefined) // "undefined" (123).toString() // "123" `${123}` // "123" (template literal) // To Boolean Boolean(0) // false Boolean("") // false Boolean(null) // false Boolean(1) // true Boolean("hello") // true Boolean([]) // true !!value // shorthand for Boolean(value) — double NOT

== vs === — The Golden Rule

// == (loose equality) — performs type coercion 5 == "5" // true (converts "5" to 5) null == undefined // true 0 == false // true // === (strict equality) — NO type coercion 5 === "5" // false (different types) null === undefined // false 0 === false // false // 🏆 RULE: ALWAYS use === and !== . Never use == and !=. // The ONLY exception: val == null checks for both null AND undefined: if (val == null) { // val is null OR undefined } // This is equivalent to: if (val === null || val === undefined) { }

6. Operators

Arithmetic Operators

let a = 10, b = 3; a + b // 13 — Addition a - b // 7 — Subtraction a * b // 30 — Multiplication a / b // 3.3333... — Division (always returns float) a % b // 1 — Modulo (remainder) a ** b // 1000 — Exponentiation (ES2016) // Increment / Decrement let x = 5; x++ // Returns 5, THEN x becomes 6 (post-increment) ++x // x becomes 7, THEN returns 7 (pre-increment) x-- // Returns 7, THEN x becomes 6 (post-decrement) --x // x becomes 5, THEN returns 5 (pre-decrement) // ⚠️ Interview trick question: let y = 5; console.log(y++ + ++y); // 5 + 7 = 12 // y++ returns 5 (then y becomes 6) // ++y makes y 7 (then returns 7)

Comparison Operators

5 > 3 // true 5 < 3 // false 5 >= 5 // true 5 <= 4 // false 5 === 5 // true (strict equality) 5 !== 3 // true (strict inequality)

Logical Operators

// AND (&&) — returns first falsy value, or last value if all truthy true && true // true true && false // false "hello" && 42 // 42 (both truthy, returns last) 0 && "hello" // 0 (first falsy) null && "hello" // null (first falsy) // OR (||) — returns first truthy value, or last value if all falsy true || false // true false || "hello" // "hello" (first truthy) 0 || "" || null // null (all falsy, returns last) // NOT (!) !true // false !0 // true !"hello" // false !!"" // false (double NOT — converts to boolean) // Nullish Coalescing (??) — ES2020 // Returns right side only if left is null or undefined (NOT other falsy values) 0 ?? 42 // 0 (0 is not null/undefined) "" ?? "default" // "" (empty string is not null/undefined) null ?? 42 // 42 undefined ?? 42 // 42 // Optional Chaining (?.) — ES2020 // Safely access nested properties without throwing let user = { address: { street: "123 Main" } }; user.address.street // "123 Main" user.phone?.number // undefined (doesn't throw even though phone is undefined) user.getAddress?.() // undefined (safely calls method if it exists) // Without optional chaining you'd need: user.phone && user.phone.number // same thing but verbose

Short-Circuit Evaluation — Used EVERYWHERE in Real Code

// && for conditional execution isLoggedIn && showDashboard(); // Only calls showDashboard() if isLoggedIn is truthy // || for default values const port = process.env.PORT || 3000; // ?? for null-safe defaults const timeout = config.timeout ?? 5000; // Real-world example: Express middleware const userId = req.user?.id; // Safely get user ID, undefined if not logged in if (!userId) { return res.status(401).json({ error: "Not authenticated" }); }

Spread and Rest Operators (...)

// SPREAD — "spreads" an iterable into individual elements // For arrays: const arr1 = [1, 2, 3]; const arr2 = [4, 5, 6]; const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6] const copy = [...arr1]; // [1, 2, 3] — shallow copy // For objects: const defaults = { theme: "dark", lang: "en", fontSize: 14 }; const userPrefs = { theme: "light", fontSize: 16 }; const merged = { ...defaults, ...userPrefs }; // { theme: "light", lang: "en", fontSize: 16 } — later spreads win // REST — "gathers" remaining elements into an array/object // In function parameters: function sum(...numbers) { // numbers is an array of all arguments return numbers.reduce((total, n) => total + n, 0); } sum(1, 2, 3, 4); // 10 // In destructuring: const [first, second, ...rest] = [1, 2, 3, 4, 5]; // first = 1, second = 2, rest = [3, 4, 5] const { name, ...otherProps } = { name: "Alice", age: 25, city: "NYC" }; // name = "Alice", otherProps = { age: 25, city: "NYC" }

7. Strings — Deep Dive

String Creation and Template Literals

// Single quotes, double quotes, template literals let s1 = 'Hello'; let s2 = "Hello"; let s3 = `Hello ${s1}`; // Template literal — allows interpolation // Multi-line strings let multiline = ` This is line 1 This is line 2 This is line 3 `; // Template literals preserve newlines // Tagged template literals (advanced — used in libraries like styled-components) function highlight(strings, ...values) { return strings.reduce((result, str, i) => { return result + str + (values[i] ? `**${values[i]}**` : ''); }, ''); } let name = "Alice"; let age = 25; highlight`Name: ${name}, Age: ${age}`; // "Name: **Alice**, Age: **25**"

Essential String Methods

let str = "Hello, World!"; // Length (property, not method) str.length // 13 // Access characters str[0] // "H" str.charAt(0) // "H" str.at(-1) // "!" (ES2022 — supports negative indexing) // Search str.indexOf("World") // 7 (first occurrence, -1 if not found) str.lastIndexOf("l") // 10 str.includes("World") // true (ES6) str.startsWith("Hello") // true (ES6) str.endsWith("!") // true (ES6) // Extract str.slice(0, 5) // "Hello" (start, end — end not included) str.slice(-6) // "orld!" (negative = from end) str.substring(0, 5) // "Hello" (like slice but no negative indices) // Transform str.toUpperCase() // "HELLO, WORLD!" str.toLowerCase() // "hello, world!" str.trim() // removes whitespace from both ends str.trimStart() // removes from start only str.trimEnd() // removes from end only str.repeat(3) // "Hello, World!Hello, World!Hello, World!" str.padStart(20, "-") // "-------Hello, World!" str.padEnd(20, "-") // "Hello, World!-------" // Replace str.replace("World", "JS") // "Hello, JS!" (first occurrence only) str.replaceAll("l", "L") // "HeLLo, WorLd!" (ES2021) str.replace(/l/g, "L") // "HeLLo, WorLd!" (regex — all occurrences) // Split (String → Array) "a,b,c,d".split(",") // ["a", "b", "c", "d"] "hello world".split(" ") // ["hello", "world"] "hello".split("") // ["h", "e", "l", "l", "o"] // Strings are IMMUTABLE — all methods return NEW strings let original = "hello"; original.toUpperCase(); // returns "HELLO" but original is still "hello"

Real-World String Operations

// 1. Generating a URL slug function slugify(title) { return title .toLowerCase() .trim() .replace(/[^\w\s-]/g, '') // Remove special chars .replace(/\s+/g, '-') // Replace spaces with hyphens .replace(/-+/g, '-'); // Replace multiple hyphens with single } slugify("Hello World! This is a Test"); // "hello-world-this-is-a-test" // 2. Extracting domain from email function getDomain(email) { return email.split("@")[1]; } getDomain("alice@gmail.com"); // "gmail.com" // 3. Masking credit card number function maskCard(cardNumber) { return cardNumber.slice(-4).padStart(cardNumber.length, "*"); } maskCard("4111111111111111"); // "************1111" // 4. Capitalizing first letter function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } capitalize("hello"); // "Hello"

8. Numbers — Deep Dive

Number Quirks You Must Know

// JavaScript uses IEEE 754 double-precision floating-point // This means: // 1. Floating-point precision issues 0.1 + 0.2 // 0.30000000000000004 (not 0.3!) 0.1 + 0.2 === 0.3 // false! // Fix: Use epsilon comparison Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true // Or work in cents (integers): let price1 = 10; // 10 cents let price2 = 20; // 20 cents let total = (price1 + price2) / 100; // $0.30 — exact! // 2. Safe integer range Number.MAX_SAFE_INTEGER // 9007199254740991 (2^53 - 1) Number.MIN_SAFE_INTEGER // -9007199254740991 // Beyond this range, precision is lost: 9007199254740992 === 9007199254740993 // true! Both are the same number! // Use BigInt for larger numbers // 3. Special values Infinity // Result of division by zero (positive) -Infinity // Negative infinity NaN // Not a Number — result of invalid math // Checking for NaN NaN === NaN // false! NaN is the only value not equal to itself Number.isNaN(NaN) // true — use this method isNaN("hello") // true (converts string first — avoid this) Number.isNaN("hello") // false (stricter — recommended) // Checking for finite Number.isFinite(42) // true Number.isFinite(Infinity) // false Number.isFinite(NaN) // false // Checking for integer Number.isInteger(42) // true Number.isInteger(42.0) // true (42.0 === 42 in JS) Number.isInteger(42.5) // false

The Math Object

Math.floor(4.7) // 4 (round down) Math.ceil(4.2) // 5 (round up) Math.round(4.5) // 5 (round to nearest) Math.trunc(4.7) // 4 (remove decimals — no rounding) Math.abs(-5) // 5 (absolute value) Math.max(1, 5, 3) // 5 Math.min(1, 5, 3) // 1 Math.pow(2, 10) // 1024 (same as 2 ** 10) Math.sqrt(16) // 4 Math.random() // Random float between 0 (inclusive) and 1 (exclusive) // Random integer between min and max (inclusive) function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } randomInt(1, 6); // Dice roll: 1-6

9. Control Flow

If / Else If / Else

let score = 85; if (score >= 90) { console.log("A"); } else if (score >= 80) { console.log("B"); // ← This runs } else if (score >= 70) { console.log("C"); } else { console.log("F"); } // Ternary operator — shorthand for simple if/else let grade = score >= 90 ? "A" : score >= 80 ? "B" : "C"; // Guard clauses — preferred pattern in professional code function processUser(user) { if (!user) return null; // Guard: exit early if (!user.isActive) return null; // Guard: exit early // Main logic — no nesting needed return `Processing ${user.name}`; }

Switch Statement

let status = "active"; switch (status) { case "active": console.log("User is active"); break; // Without break, execution "falls through" to next case! case "inactive": console.log("User is inactive"); break; case "banned": console.log("User is banned"); break; default: console.log("Unknown status"); } // Fall-through can be intentional: let day = "Monday"; switch (day) { case "Monday": case "Tuesday": case "Wednesday": case "Thursday": case "Friday": console.log("Weekday"); break; case "Saturday": case "Sunday": console.log("Weekend"); break; }

Loops

// 1. for loop — when you know the count for (let i = 0; i < 5; i++) { console.log(i); // 0, 1, 2, 3, 4 } // 2. while loop — when you don't know the count let input = ""; while (input !== "quit") { // keep going until user types "quit" input = getInput(); // hypothetical function } // 3. do...while — runs at least once let attempts = 0; do { attempts++; // try something } while (attempts < 3); // 4. for...of — iterate over ITERABLE values (arrays, strings, maps, sets) let colors = ["red", "green", "blue"]; for (let color of colors) { console.log(color); // "red", "green", "blue" } for (let char of "hello") { console.log(char); // "h", "e", "l", "l", "o" } // 5. for...in — iterate over object KEYS (also works on arrays but DON'T use it) let person = { name: "Alice", age: 25, city: "NYC" }; for (let key in person) { console.log(`${key}: ${person[key]}`); // "name: Alice", "age: 25", "city: NYC" } // ⚠️ DON'T use for...in on arrays — it iterates over indices as STRINGS // and can include inherited properties let arr = [10, 20, 30]; for (let index in arr) { console.log(typeof index); // "string"! Not a number! } // break and continue for (let i = 0; i < 10; i++) { if (i === 3) continue; // Skip this iteration if (i === 7) break; // Exit the loop entirely console.log(i); // 0, 1, 2, 4, 5, 6 } // Labeled loops (rare but asked in interviews) outer: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { if (i === 1 && j === 1) break outer; // Breaks the outer loop console.log(i, j); } } // 0 0, 0 1, 0 2, 1 0

10. Functions — The Heart of JavaScript

Functions in JavaScript are first-class citizens — they can be:

  • Assigned to variables
  • Passed as arguments to other functions
  • Returned from other functions
  • Stored in data structures

This is the single most important concept that separates JavaScript from many other languages.

Function Declarations vs Expressions

// Function Declaration — hoisted (can be called before definition) greet("Alice"); // ✅ Works! Function declarations are hoisted. function greet(name) { return `Hello, ${name}!`; } // Function Expression — NOT hoisted // sayHi("Bob"); // ❌ ReferenceError: Cannot access 'sayHi' before initialization const sayHi = function(name) { return `Hi, ${name}!`; }; sayHi("Bob"); // ✅ Works here // Named Function Expression — useful for recursion and stack traces const factorial = function fact(n) { if (n <= 1) return 1; return n * fact(n - 1); // Can refer to itself by name };

Arrow Functions (ES6) — Modern Syntax

// Regular function function add(a, b) { return a + b; } // Arrow function (equivalent) const add = (a, b) => { return a + b; }; // If body is a single expression, you can omit {} and return const add = (a, b) => a + b; // If single parameter, you can omit () const double = x => x * 2; // If no parameters, you need () const getTimestamp = () => Date.now(); // Returning an object literal? Wrap in () const makeUser = (name, age) => ({ name, age }); // Without () it'd be a code block // ⚠️ Arrow functions have KEY DIFFERENCES from regular functions: // 1. They do NOT have their own `this` (they inherit from parent scope) // 2. They do NOT have `arguments` object // 3. They CANNOT be used as constructors (no `new` keyword) // 4. They do NOT have `prototype` property // We'll explore `this` differences in detail in section 16

Parameters and Arguments

// Default parameters (ES6) function greet(name = "World", greeting = "Hello") { return `${greeting}, ${name}!`; } greet(); // "Hello, World!" greet("Alice"); // "Hello, Alice!" greet("Alice", "Hi"); // "Hi, Alice!" // Rest parameters (...) — gather remaining args into array function sum(first, ...rest) { console.log(first); // 1 console.log(rest); // [2, 3, 4, 5] return rest.reduce((total, n) => total + n, first); } sum(1, 2, 3, 4, 5); // 15 // The old `arguments` object (avoid in modern code) function oldSum() { console.log(arguments); // { '0': 1, '1': 2, '2': 3 } — array-LIKE, not real array console.log(arguments.length); // 3 // To use array methods, convert first: return Array.from(arguments).reduce((t, n) => t + n, 0); } oldSum(1, 2, 3); // 6 // Destructured parameters — very common in real codebases function createUser({ name, age, role = "user" }) { return { name, age, role, createdAt: Date.now() }; } createUser({ name: "Alice", age: 25 }); // { name: "Alice", age: 25, role: "user", ... }

Higher-Order Functions — Functions That Take/Return Functions

// A higher-order function is a function that: // 1. Takes a function as an argument, OR // 2. Returns a function // Example 1: Function as argument (callback pattern) function doOperation(a, b, operation) { return operation(a, b); } doOperation(5, 3, (a, b) => a + b); // 8 doOperation(5, 3, (a, b) => a * b); // 15 // Example 2: Function returning function (factory pattern) function createMultiplier(factor) { return function(number) { return number * factor; }; } const double = createMultiplier(2); const triple = createMultiplier(3); double(5); // 10 triple(5); // 15 // Example 3: Real-world — Express middleware factory function requireRole(role) { return function(req, res, next) { if (req.user.role !== role) { return res.status(403).json({ error: "Forbidden" }); } next(); }; } // Usage: app.get("/admin", requireRole("admin"), adminHandler); // Example 4: Real-world — Rate limiter factory function createRateLimiter(maxRequests, windowMs) { const requests = new Map(); return function(userId) { const now = Date.now(); const userRequests = requests.get(userId) || []; // Remove old requests outside the window const recentRequests = userRequests.filter(time => now - time < windowMs); if (recentRequests.length >= maxRequests) { return false; // Rate limited } recentRequests.push(now); requests.set(userId, recentRequests); return true; // Allowed }; } const limiter = createRateLimiter(100, 60000); // 100 requests per minute limiter("user123"); // true

IIFE — Immediately Invoked Function Expression

// A function that runs immediately after being defined (function() { console.log("I run immediately!"); // Variables here don't pollute global scope var secret = "hidden"; })(); // console.log(secret); // ReferenceError // Arrow function IIFE (() => { console.log("Arrow IIFE"); })(); // With parameters ((name) => { console.log(`Hello, ${name}!`); })("Alice"); // Real-world use: Module pattern (before ES6 modules) const counter = (() => { let count = 0; // Private variable return { increment: () => ++count, decrement: () => --count, getCount: () => count }; })(); counter.increment(); // 1 counter.increment(); // 2 counter.getCount(); // 2 // count is not accessible from outside!

11. Scope — Where Variables Live

Scope determines where a variable is accessible in your code.

Types of Scope

// 1. GLOBAL SCOPE — accessible everywhere var globalVar = "I'm global"; // var in global scope let globalLet = "I'm also global"; // let/const in global scope // In Node.js, each file has its own module scope (not truly global) // To make something truly global in Node.js: global.myVar = "value"; // 2. FUNCTION SCOPE — variables declared inside a function function myFunc() { var localVar = "I'm local to myFunc"; let localLet = "Me too"; console.log(localVar); // ✅ Accessible } // console.log(localVar); // ❌ ReferenceError // console.log(localLet); // ❌ ReferenceError // 3. BLOCK SCOPE — let and const are block-scoped { let blockLet = "I'm in a block"; const blockConst = "Me too"; var blockVar = "But I'm NOT block-scoped!"; } // console.log(blockLet); // ❌ ReferenceError // console.log(blockConst); // ❌ ReferenceError console.log(blockVar); // ✅ "But I'm NOT block-scoped!" — var ignores blocks! // 4. MODULE SCOPE (Node.js / ES Modules) // Each file in Node.js is a module with its own scope // Variables declared in a file are NOT global — they're module-scoped

Lexical Scope (Static Scope) — How JavaScript Resolves Variables

// JavaScript uses LEXICAL SCOPING — a function's scope is determined // by WHERE it is WRITTEN (defined), not where it is CALLED. let x = "global"; function outer() { let x = "outer"; function inner() { console.log(x); // "outer" — inner looks up its scope chain } inner(); } outer(); // "outer" // The scope chain: // inner() → looks for x → not found → goes to outer() → found "outer" // Even if inner was called from somewhere else, it would still use // the scope where it was DEFINED (outer), not where it was called.

Scope Chain Visualization

// Think of scope as nested boxes. Each function creates a new box. // Variables are looked up from inner → outer → global /* ┌─────────────────────────── Global Scope ───────────────────────────┐ │ let name = "Global" │ │ │ │ ┌─────────────────────── outer() Scope ─────────────────────────┐ │ │ │ let name = "Outer" │ │ │ │ │ │ │ │ ┌─────────────────── inner() Scope ────────────────────────┐ │ │ │ │ │ console.log(name); // Looks here first → not found │ │ │ │ │ │ // Goes to outer → found "Outer" │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ */ function outer() { let name = "Outer"; function inner() { // name is not defined in inner's scope // JavaScript goes up the scope chain to outer() console.log(name); // "Outer" } inner(); }

12. Hoisting — Why Order Doesn't Always Matter

Hoisting is JavaScript's behavior of moving declarations to the top of their scope during compilation (before execution).

How Different Declarations Are Hoisted

// 1. var — hoisted and initialized with undefined console.log(a); // undefined (not an error!) var a = 5; console.log(a); // 5 // What JavaScript actually does: // var a; // Declaration hoisted to top, initialized as undefined // console.log(a); // undefined // a = 5; // Assignment stays in place // console.log(a); // 5 // 2. let / const — hoisted but NOT initialized (Temporal Dead Zone) // console.log(b); // ❌ ReferenceError: Cannot access 'b' before initialization let b = 5; console.log(b); // 5 // The time between entering the scope and the declaration is called // the "Temporal Dead Zone" (TDZ). Accessing the variable in TDZ throws an error. /* TDZ starts ──┐ │ console.log(b); // ❌ In TDZ! TDZ ends ────┤ let b = 5; // Declaration — TDZ ends here │ console.log(b); // ✅ 5 */ // 3. function declarations — fully hoisted (both declaration AND body) greet(); // ✅ "Hello!" — works because function declarations are fully hoisted function greet() { console.log("Hello!"); } // 4. function expressions — NOT hoisted (they follow var/let/const rules) // sayHi(); // ❌ TypeError: sayHi is not a function (if var) // sayHi(); // ❌ ReferenceError (if let/const) var sayHi = function() { console.log("Hi!"); }; // 5. class declarations — hoisted but in TDZ (like let/const) // const p = new Person(); // ❌ ReferenceError: Cannot access 'Person' before initialization class Person { constructor(name) { this.name = name; } }

🎯 Interview Question: What's the output?

var x = 1; function foo() { console.log(x); // What prints here? var x = 2; console.log(x); // What prints here? } foo(); // Answer: // First console.log: undefined (not 1!) // Because var x inside foo is hoisted to the top of foo's scope, // shadowing the global x. But the assignment hasn't happened yet. // Second console.log: 2 // It's as if the code was: function foo() { var x; // Hoisted declaration (shadows global x) console.log(x); // undefined x = 2; // Assignment console.log(x); // 2 }

13. Closures — The Most Important Concept

A closure is when a function "remembers" and can access variables from its outer scope even after the outer function has finished executing.

Understanding Closures Step by Step

// Step 1: A simple function inside a function function outer() { let message = "Hello!"; function inner() { console.log(message); // inner can access outer's variables } inner(); // "Hello!" } outer(); // Step 2: Returning the inner function — THIS IS A CLOSURE function outer() { let message = "Hello!"; function inner() { console.log(message); } return inner; // Return the function itself (not the result) } const myFunc = outer(); // outer() runs and returns inner // At this point, outer() has finished executing. // Its local variable `message` should be garbage collected, right? myFunc(); // "Hello!" — BUT IT STILL WORKS! // inner() still has access to `message` even though outer() is done. // This is a CLOSURE. inner() "closed over" the variable `message`.

Why Do Closures Exist?

The inner function keeps a reference to its outer scope's variables. As long as the inner function exists, those variables won't be garbage collected.

// Think of it like this: // When outer() returns inner(), inner carries a "backpack" containing // all the variables it needs from outer's scope. function createCounter() { let count = 0; // This variable lives in the closure's "backpack" return { increment: () => ++count, decrement: () => --count, getCount: () => count }; } const counter = createCounter(); counter.increment(); // 1 counter.increment(); // 2 counter.increment(); // 3 counter.decrement(); // 2 counter.getCount(); // 2 // `count` is not accessible from outside — it's truly private! // console.log(count); // ❌ ReferenceError // console.log(counter.count); // ❌ undefined // Each call to createCounter() creates a NEW closure with its own `count` const counter2 = createCounter(); counter2.increment(); // 1 — independent from counter!

Real-World Closure Examples

// 1. Data Privacy / Encapsulation function createBankAccount(initialBalance) { let balance = initialBalance; // Private — can't be accessed directly const transactions = []; // Private return { deposit(amount) { if (amount <= 0) throw new Error("Amount must be positive"); balance += amount; transactions.push({ type: "deposit", amount, date: new Date() }); return balance; }, withdraw(amount) { if (amount <= 0) throw new Error("Amount must be positive"); if (amount > balance) throw new Error("Insufficient funds"); balance -= amount; transactions.push({ type: "withdrawal", amount, date: new Date() }); return balance; }, getBalance() { return balance; }, getStatement() { return [...transactions]; // Return a copy, not the original } }; } const account = createBankAccount(1000); account.deposit(500); // 1500 account.withdraw(200); // 1300 account.getBalance(); // 1300 // account.balance // undefined — can't access directly! // account.transactions // undefined — can't access directly! // 2. Function Factory function createLogger(prefix) { return function(message) { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] [${prefix}] ${message}`); }; } const dbLogger = createLogger("DATABASE"); const apiLogger = createLogger("API"); const authLogger = createLogger("AUTH"); dbLogger("Connected to MongoDB"); // [2024-...] [DATABASE] Connected to MongoDB apiLogger("GET /users — 200"); // [2024-...] [API] GET /users — 200 authLogger("Login attempt for alice"); // [2024-...] [AUTH] Login attempt for alice // 3. Memoization — Caching expensive computations function memoize(fn) { const cache = {}; // Closure over cache return function(...args) { const key = JSON.stringify(args); if (key in cache) { console.log("Cache hit!"); return cache[key]; } console.log("Computing..."); const result = fn(...args); cache[key] = result; return result; }; } const expensiveAdd = memoize((a, b) => { // Simulating expensive computation return a + b; }); expensiveAdd(1, 2); // "Computing..." → 3 expensiveAdd(1, 2); // "Cache hit!" → 3 (from cache!) expensiveAdd(3, 4); // "Computing..." → 7 // 4. Event handlers (browser context) function setupButton(buttonId, message) { const button = document.getElementById(buttonId); let clickCount = 0; // Each button has its own count via closure button.addEventListener("click", () => { clickCount++; console.log(`${message} — Clicked ${clickCount} times`); }); } setupButton("btn1", "Button 1"); setupButton("btn2", "Button 2");

The Classic Closure Bug — Loop + var

// THE BUG: for (var i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 1000); } // Expected: 0, 1, 2 // Actual: 3, 3, 3 // WHY? Because var is function-scoped. There's only ONE `i`. // By the time setTimeout callbacks run (after 1 second), the loop is done and i === 3. // All three closures reference the SAME `i`. // FIX 1: Use let (block-scoped — creates new i for each iteration) for (let i = 0; i < 3; i++) { setTimeout(() => { console.log(i); // 0, 1, 2 ✅ }, 1000); } // FIX 2: Use IIFE to create a new scope for (var i = 0; i < 3; i++) { ((j) => { setTimeout(() => { console.log(j); // 0, 1, 2 ✅ }, 1000); })(i); } // FIX 3: Use setTimeout's third argument for (var i = 0; i < 3; i++) { setTimeout((j) => { console.log(j); // 0, 1, 2 ✅ }, 1000, i); }

14. Objects — Deep Dive

Creating Objects

// 1. Object Literal (most common) const user = { name: "Alice", age: 25, email: "alice@example.com", "has-hyphen": true, // Keys with special chars need quotes 123: "numeric key" // Numeric keys are converted to strings }; // 2. Object Constructor (rarely used) const user2 = new Object(); user2.name = "Bob"; // 3. Object.create() — creates object with specific prototype const personProto = { greet() { return `Hi, I'm ${this.name}`; } }; const user3 = Object.create(personProto); user3.name = "Charlie"; user3.greet(); // "Hi, I'm Charlie" // 4. Factory Function (common pattern) function createUser(name, age) { return { name, age, greet() { return `Hi, I'm ${name}`; } }; }

Accessing and Modifying Properties

const user = { name: "Alice", age: 25 }; // Dot notation (preferred) user.name // "Alice" user.age = 26; // Modify user.city = "NYC"; // Add new property // Bracket notation (for dynamic keys or special characters) user["name"] // "Alice" let key = "age"; user[key] // 25 // Computed property names (ES6) const field = "email"; const user2 = { [field]: "alice@example.com", // email: "alice@example.com" [`${field}Verified`]: true // emailVerified: true }; // Deleting properties delete user.city; // Removes the property // Checking if property exists "name" in user // true (checks own + inherited) user.hasOwnProperty("name") // true (checks own only) Object.hasOwn(user, "name") // true (ES2022, preferred over hasOwnProperty)

Object Shorthand and Destructuring (ES6)

// Property Shorthand — when variable name matches property name const name = "Alice"; const age = 25; // Old way const user = { name: name, age: age }; // Shorthand (ES6) const user = { name, age }; // Same thing! // Method Shorthand const user = { name: "Alice", // Old way greet: function() { return "Hi!"; }, // Shorthand (ES6) greet() { return "Hi!"; } }; // DESTRUCTURING — extract properties into variables const user = { name: "Alice", age: 25, city: "NYC", country: "USA" }; // Basic destructuring const { name, age } = user; console.log(name); // "Alice" console.log(age); // 25 // Renaming const { name: userName, age: userAge } = user; console.log(userName); // "Alice" // Default values const { name, role = "user" } = user; // role doesn't exist, gets default console.log(role); // "user" // Nested destructuring const config = { server: { host: "localhost", port: 3000 }, database: { url: "mongodb://localhost/mydb" } }; const { server: { host, port }, database: { url } } = config; console.log(host); // "localhost" console.log(port); // 3000 // Rest in destructuring const { name, ...rest } = user; console.log(rest); // { age: 25, city: "NYC", country: "USA" } // Destructuring in function parameters — VERY common in real code function createServer({ host = "localhost", port = 3000, ssl = false } = {}) { console.log(`Server: ${ssl ? "https" : "http"}://${host}:${port}`); } createServer({ port: 8080 }); // "Server: http://localhost:8080" createServer(); // "Server: http://localhost:3000"

Essential Object Methods

const user = { name: "Alice", age: 25, city: "NYC" }; // Object.keys() — get array of keys Object.keys(user); // ["name", "age", "city"] // Object.values() — get array of values Object.values(user); // ["Alice", 25, "NYC"] // Object.entries() — get array of [key, value] pairs Object.entries(user); // [["name", "Alice"], ["age", 25], ["city", "NYC"]] // Object.fromEntries() — reverse of entries (create object from pairs) const entries = [["name", "Bob"], ["age", 30]]; Object.fromEntries(entries); // { name: "Bob", age: 30 } // Object.assign() — merge objects (mutates target!) const target = { a: 1 }; const source1 = { b: 2 }; const source2 = { c: 3 }; Object.assign(target, source1, source2); // { a: 1, b: 2, c: 3 } // ⚠️ target is modified! Use spread for immutable merge: { ...target, ...source1, ...source2 } // Object.freeze() — make object completely immutable (shallow!) const frozen = Object.freeze({ name: "Alice", address: { city: "NYC" } }); frozen.name = "Bob"; // Silently fails (no error in non-strict mode) frozen.address.city = "LA"; // ✅ This WORKS because freeze is shallow! // Object.seal() — can modify existing props but can't add/delete const sealed = Object.seal({ name: "Alice", age: 25 }); sealed.name = "Bob"; // ✅ Can modify sealed.city = "NYC"; // ❌ Silently fails — can't add new properties delete sealed.name; // ❌ Silently fails — can't delete properties // Iterating over objects const scores = { math: 95, english: 88, science: 92 }; // Method 1: for...in for (let subject in scores) { console.log(`${subject}: ${scores[subject]}`); } // Method 2: Object.entries() + for...of for (let [subject, score] of Object.entries(scores)) { console.log(`${subject}: ${score}`); } // Method 3: Object.keys() + forEach Object.keys(scores).forEach(subject => { console.log(`${subject}: ${scores[subject]}`); });

Shallow vs Deep Copy

const original = { name: "Alice", scores: [95, 88, 92], address: { city: "NYC", zip: "10001" } }; // SHALLOW COPY — only copies first level const shallow1 = { ...original }; const shallow2 = Object.assign({}, original); shallow1.name = "Bob"; // Doesn't affect original ✅ shallow1.scores.push(100); // AFFECTS original! ❌ (same array reference) shallow1.address.city = "LA"; // AFFECTS original! ❌ (same object reference) // DEEP COPY — copies everything recursively const deep1 = JSON.parse(JSON.stringify(original)); // ⚠️ JSON method loses: functions, undefined, Infinity, NaN, Date (becomes string), RegExp, Maps, Sets const deep2 = structuredClone(original); // Modern way (Node 17+, all modern browsers) // ✅ Handles most types correctly (but not functions) deep2.scores.push(100); // Doesn't affect original ✅ deep2.address.city = "LA"; // Doesn't affect original ✅

15. Arrays — Deep Dive

Arrays in JavaScript are objects with integer keys and special behavior.

Creating Arrays

const arr1 = [1, 2, 3]; // Array literal (preferred) const arr2 = new Array(3); // [empty × 3] — creates 3 empty slots const arr3 = Array.of(3); // [3] — creates array with element 3 const arr4 = Array.from("hello"); // ["h", "e", "l", "l", "o"] const arr5 = Array.from({ length: 5 }, (_, i) => i * 2); // [0, 2, 4, 6, 8]

Mutating Methods (Modify the original array)

let arr = [1, 2, 3, 4, 5]; // push / pop — end of array arr.push(6); // [1, 2, 3, 4, 5, 6] — returns new length (6) arr.pop(); // [1, 2, 3, 4, 5] — returns removed element (6) // unshift / shift — beginning of array arr.unshift(0); // [0, 1, 2, 3, 4, 5] — returns new length (6) arr.shift(); // [1, 2, 3, 4, 5] — returns removed element (0) // splice — insert, remove, or replace at any position arr.splice(2, 1); // Removes 1 element at index 2 → [1, 2, 4, 5], returns [3] arr.splice(2, 0, 3); // Inserts 3 at index 2 → [1, 2, 3, 4, 5], returns [] arr.splice(1, 2, 20, 30); // Replace 2 elements at index 1 → [1, 20, 30, 4, 5] // sort — sorts IN PLACE (mutates!) let nums = [10, 5, 8, 1, 3]; nums.sort(); // [1, 10, 3, 5, 8] ← WRONG! Sorts as strings! nums.sort((a, b) => a - b); // [1, 3, 5, 8, 10] ← Correct numeric sort // reverse — reverses IN PLACE arr.reverse(); // [5, 4, 3, 2, 1] // fill [1, 2, 3, 4].fill(0); // [0, 0, 0, 0] [1, 2, 3, 4].fill(0, 1, 3); // [1, 0, 0, 4] — fill from index 1 to 3

Non-Mutating Methods (Return new array/value)

const arr = [1, 2, 3, 4, 5]; // slice — extract a portion arr.slice(1, 3); // [2, 3] (start inclusive, end exclusive) arr.slice(-2); // [4, 5] (last 2 elements) arr.slice(); // [1, 2, 3, 4, 5] (shallow copy) // concat arr.concat([6, 7]); // [1, 2, 3, 4, 5, 6, 7] // join — array to string arr.join(", "); // "1, 2, 3, 4, 5" arr.join("-"); // "1-2-3-4-5" // flat — flatten nested arrays [1, [2, [3, [4]]]].flat(); // [1, 2, [3, [4]]] — one level [1, [2, [3, [4]]]].flat(2); // [1, 2, 3, [4]] — two levels [1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4] — all levels // includes — check if element exists arr.includes(3); // true arr.includes(6); // false // indexOf / lastIndexOf — find index of element arr.indexOf(3); // 2 (first occurrence) arr.lastIndexOf(3); // 2 // at() — access by index (supports negative) — ES2022 arr.at(0); // 1 arr.at(-1); // 5 (last element) arr.at(-2); // 4

The BIG 5 — Array Methods You'll Use Every Day

These are functional programming methods. They take a callback function and apply it to each element.

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // 1. map() — Transform each element → returns NEW array const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] // 2. filter() — Keep elements that pass a test → returns NEW array const evens = numbers.filter(n => n % 2 === 0); // [2, 4, 6, 8, 10] // 3. reduce() — Accumulate all elements into a single value const sum = numbers.reduce((accumulator, current) => accumulator + current, 0); // 55 // 4. find() — Find the FIRST element that passes a test const firstEven = numbers.find(n => n % 2 === 0); // 2 // 5. forEach() — Execute a function for each element (no return value) numbers.forEach(n => console.log(n)); // Prints 1 through 10

Real-World Array Operations

// Dataset: array of user objects (very common in APIs) const users = [ { id: 1, name: "Alice", age: 25, role: "admin", active: true }, { id: 2, name: "Bob", age: 30, role: "user", active: false }, { id: 3, name: "Charlie", age: 35, role: "user", active: true }, { id: 4, name: "Diana", age: 28, role: "admin", active: true }, { id: 5, name: "Eve", age: 22, role: "user", active: true }, ]; // 1. Get names of all active users const activeNames = users .filter(user => user.active) .map(user => user.name); // ["Alice", "Charlie", "Diana", "Eve"] // 2. Find the oldest user const oldest = users.reduce((max, user) => user.age > max.age ? user : max); // { id: 3, name: "Charlie", age: 35, ... } // 3. Group users by role const byRole = users.reduce((groups, user) => { const role = user.role; groups[role] = groups[role] || []; groups[role].push(user); return groups; }, {}); // { admin: [{Alice}, {Diana}], user: [{Bob}, {Charlie}, {Eve}] } // Modern way (ES2024): const byRole2 = Object.groupBy(users, user => user.role); // 4. Check if ALL users are over 18 const allAdults = users.every(user => user.age >= 18); // true // 5. Check if ANY user is admin const hasAdmin = users.some(user => user.role === "admin"); // true // 6. Count users by role const roleCounts = users.reduce((counts, user) => { counts[user.role] = (counts[user.role] || 0) + 1; return counts; }, {}); // { admin: 2, user: 3 } // 7. Sort users by age (descending) const sortedByAge = [...users].sort((a, b) => b.age - a.age); // Note: [...users] creates a copy so we don't mutate the original // 8. Paginate (get page 2, 2 items per page) const page = 2; const perPage = 2; const paginated = users.slice((page - 1) * perPage, page * perPage); // [{Charlie}, {Diana}] // 9. Remove duplicates from an array const withDupes = [1, 2, 2, 3, 3, 3, 4]; const unique = [...new Set(withDupes)]; // [1, 2, 3, 4] // 10. Flatten and process nested data const departments = [ { name: "Engineering", employees: ["Alice", "Bob"] }, { name: "Design", employees: ["Charlie", "Diana"] }, ]; const allEmployees = departments.flatMap(dept => dept.employees); // ["Alice", "Bob", "Charlie", "Diana"]

Chaining Methods — The Power Pattern

// You can chain array methods because each returns a new array const transactions = [ { type: "credit", amount: 100, date: "2024-01-15" }, { type: "debit", amount: 50, date: "2024-01-16" }, { type: "credit", amount: 200, date: "2024-01-17" }, { type: "debit", amount: 75, date: "2024-01-18" }, { type: "credit", amount: 150, date: "2024-01-19" }, ]; // Calculate total of all credit transactions above 100 const bigCreditTotal = transactions .filter(t => t.type === "credit") // Keep only credits .filter(t => t.amount > 100) // Keep only above 100 .map(t => t.amount) // Extract just the amounts .reduce((sum, amt) => sum + amt, 0); // Sum them up // 350 // Same thing more efficiently: const bigCreditTotal2 = transactions.reduce((sum, t) => { if (t.type === "credit" && t.amount > 100) { return sum + t.amount; } return sum; }, 0); // 350

16. The this Keyword

this is one of JavaScript's most confusing concepts. Its value depends on HOW a function is called, not where it's defined.

Rule 1: Global Context

// In browser: console.log(this); // Window object // In Node.js: console.log(this); // {} (empty object — it's module.exports) // In strict mode: "use strict"; function show() { console.log(this); // undefined (not Window) }

Rule 2: Object Method — this is the object

const user = { name: "Alice", greet() { console.log(this.name); // "Alice" — this = user } }; user.greet(); // "Alice" // ⚠️ But if you extract the method: const greetFunc = user.greet; greetFunc(); // undefined! this is no longer user — it's global/undefined

Rule 3: call, apply, bind — Manually Set this

function greet(greeting, punctuation) { console.log(`${greeting}, ${this.name}${punctuation}`); } const alice = { name: "Alice" }; const bob = { name: "Bob" }; // call — invokes immediately, args passed individually greet.call(alice, "Hello", "!"); // "Hello, Alice!" greet.call(bob, "Hi", "."); // "Hi, Bob." // apply — invokes immediately, args passed as array greet.apply(alice, ["Hello", "!"]); // "Hello, Alice!" // bind — returns NEW function with `this` permanently set const greetAlice = greet.bind(alice); greetAlice("Hey", "!"); // "Hey, Alice!" greetAlice("Yo", "?"); // "Yo, Alice?" // Real-world example: borrowing methods const numbers = { values: [1, 2, 3, 4, 5] }; // Array.prototype.slice.call(numbers.values, 1, 3) // Or: [].slice.call(arguments) — converting arguments to array (old pattern)

Rule 4: Arrow Functions — this is Inherited from Parent

// Arrow functions do NOT have their own `this`. // They use `this` from the enclosing lexical scope. const user = { name: "Alice", // Regular method — `this` = user greet() { console.log(this.name); // "Alice" // Problem: regular function inside method loses `this` setTimeout(function() { console.log(this.name); // undefined! `this` is global/undefined here }, 100); // Solution: arrow function inherits `this` from greet() setTimeout(() => { console.log(this.name); // "Alice" ✅ arrow function uses greet's `this` }, 100); }, // ⚠️ DON'T use arrow functions as methods! badGreet: () => { console.log(this.name); // undefined! Arrow inherits from module scope, not user } };

Rule 5: Constructor / newthis is the New Object

function Person(name, age) { // `this` = new empty object {} this.name = name; this.age = age; // implicitly returns `this` } const alice = new Person("Alice", 25); console.log(alice.name); // "Alice" // What `new` does behind the scenes: // 1. Creates a new empty object: {} // 2. Sets the prototype: {}.__proto__ = Person.prototype // 3. Calls Person() with `this` = the new object // 4. Returns the new object (unless the function returns a different object)

this Priority (Highest to Lowest)

1. new binding → this = new object 2. explicit binding → call/apply/bind → this = specified object 3. implicit binding → obj.method()this = obj 4. default binding → standalone function → this = global/undefined 5. arrow function → this = lexically inherited (cannot be overridden)

🎯 Interview Question: What's the output?

const obj = { name: "Object", getName: function() { return this.name; }, getNameArrow: () => { return this.name; } }; console.log(obj.getName()); // "Object" — implicit binding console.log(obj.getNameArrow()); // undefined — arrow inherits module scope's this const fn = obj.getName; console.log(fn()); // undefined — default binding (lost implicit) const boundFn = obj.getName.bind(obj); console.log(boundFn()); // "Object" — explicit binding

17. Prototypes & The Prototype Chain

Every object in JavaScript has an internal link to another object called its prototype. This is how JavaScript implements inheritance.

Understanding the Prototype Chain

const animal = { type: "Animal", eat() { return `${this.name} is eating`; } }; const dog = Object.create(animal); // dog's prototype is animal dog.name = "Buddy"; dog.bark = function() { return "Woof!"; }; dog.bark(); // "Woof!" — found on dog itself dog.eat(); // "Buddy is eating" — NOT on dog, found on prototype (animal) dog.type; // "Animal" — from prototype dog.toString(); // "[object Object]" — from Object.prototype (top of chain) // The chain: // dog → animal → Object.prototype → null // dog.bark → found on dog ✅ // dog.eat → not on dog → check animal → found ✅ // dog.hasOwnProperty → not on dog → not on animal → check Object.prototype → found ✅ // dog.nonExistent → not on dog → not on animal → not on Object.prototype → undefined

__proto__ vs prototype

// __proto__ — the link FROM an object TO its prototype // .prototype — the property ON a constructor function function Person(name) { this.name = name; } Person.prototype.greet = function() { return `Hi, I'm ${this.name}`; }; const alice = new Person("Alice"); // alice.__proto__ === Person.prototype (true) // Person.prototype.__proto__ === Object.prototype (true) // Object.prototype.__proto__ === null (end of chain) alice.greet(); // "Hi, I'm Alice" — found on Person.prototype alice.hasOwnProperty("name"); // true — found on Object.prototype // Modern way to get/set prototype: Object.getPrototypeOf(alice) === Person.prototype; // true

Why Prototypes Matter

// Methods defined on prototype are SHARED by all instances (memory efficient) function User(name) { this.name = name; // DON'T put methods here — each instance gets its own copy! // this.greet = function() { return `Hi, ${this.name}`; }; // Wasteful! } // DO put methods on prototype — all instances share one copy User.prototype.greet = function() { return `Hi, I'm ${this.name}`; }; const user1 = new User("Alice"); const user2 = new User("Bob"); user1.greet === user2.greet; // true — same function in memory!

18. Classes

ES6 classes are syntactic sugar over prototypes. They don't introduce a new inheritance model.

Basic Class Syntax

class User { // Constructor — called when you do `new User()` constructor(name, email) { this.name = name; this.email = email; this.createdAt = new Date(); } // Instance methods (go on prototype) greet() { return `Hi, I'm ${this.name}`; } getEmail() { return this.email; } // Static methods (called on class itself, not instances) static fromJSON(json) { const data = JSON.parse(json); return new User(data.name, data.email); } // Getter get info() { return `${this.name} (${this.email})`; } // Setter set displayName(name) { if (name.length < 2) throw new Error("Name too short"); this.name = name; } } const alice = new User("Alice", "alice@example.com"); alice.greet(); // "Hi, I'm Alice" alice.info; // "Alice (alice@example.com)" — no parentheses (getter) alice.displayName = "Al"; // Throws Error (setter validates) // Static method const bob = User.fromJSON('{"name":"Bob","email":"bob@example.com"}');

Inheritance with extends

class Animal { constructor(name, sound) { this.name = name; this.sound = sound; } speak() { return `${this.name} says ${this.sound}!`; } } class Dog extends Animal { constructor(name, breed) { super(name, "Woof"); // MUST call super() before using `this` this.breed = breed; } // Override parent method speak() { return `${super.speak()} (${this.breed})`; // Call parent's speak() } // New method fetch(item) { return `${this.name} fetches the ${item}!`; } } const buddy = new Dog("Buddy", "Golden Retriever"); buddy.speak(); // "Buddy says Woof! (Golden Retriever)" buddy.fetch("ball"); // "Buddy fetches the ball!" buddy instanceof Dog; // true buddy instanceof Animal; // true

Private Fields and Methods (ES2022)

class BankAccount { // Private fields — start with # #balance; #transactions = []; #accountNumber; constructor(accountNumber, initialBalance) { this.#accountNumber = accountNumber; this.#balance = initialBalance; } // Public method deposit(amount) { this.#validateAmount(amount); this.#balance += amount; this.#transactions.push({ type: "deposit", amount }); return this.#balance; } withdraw(amount) { this.#validateAmount(amount); if (amount > this.#balance) throw new Error("Insufficient funds"); this.#balance -= amount; this.#transactions.push({ type: "withdrawal", amount }); return this.#balance; } // Private method — cannot be called from outside #validateAmount(amount) { if (typeof amount !== "number" || amount <= 0) { throw new Error("Invalid amount"); } } get balance() { return this.#balance; } getStatement() { return [...this.#transactions]; // Return copy } } const account = new BankAccount("ACC001", 1000); account.deposit(500); // 1500 account.withdraw(200); // 1300 account.balance; // 1300 // account.#balance; // ❌ SyntaxError: Private field '#balance' // account.#validateAmount(100); // ❌ SyntaxError: Private method

Real-World Class Example: Event Emitter

// This is similar to how Node.js EventEmitter works internally class EventEmitter { #listeners = {}; on(event, callback) { if (!this.#listeners[event]) { this.#listeners[event] = []; } this.#listeners[event].push(callback); return this; // Enable chaining } off(event, callback) { if (!this.#listeners[event]) return this; this.#listeners[event] = this.#listeners[event].filter(cb => cb !== callback); return this; } emit(event, ...args) { if (!this.#listeners[event]) return false; this.#listeners[event].forEach(callback => callback(...args)); return true; } once(event, callback) { const wrapper = (...args) => { callback(...args); this.off(event, wrapper); }; this.on(event, wrapper); return this; } } // Usage const emitter = new EventEmitter(); emitter.on("message", (msg) => console.log(`Received: ${msg}`)); emitter.on("message", (msg) => console.log(`Logging: ${msg}`)); emitter.once("connect", () => console.log("Connected!")); // Only fires once emitter.emit("connect"); // "Connected!" emitter.emit("connect"); // Nothing — once listener was removed emitter.emit("message", "Hello!"); // "Received: Hello!" + "Logging: Hello!"

19. Error Handling

try / catch / finally

try { // Code that might throw an error const data = JSON.parse("invalid json"); } catch (error) { // Handle the error console.error("Parse failed:", error.message); // error.message — human-readable error description // error.name — error type (e.g., "SyntaxError") // error.stack — full stack trace } finally { // ALWAYS runs, whether error occurred or not // Used for cleanup (closing files, connections, etc.) console.log("Cleanup complete"); }

Error Types

// Built-in error types: new Error("Generic error"); new SyntaxError("Invalid syntax"); new TypeError("Expected a function"); new ReferenceError("Variable not defined"); new RangeError("Number out of range"); new URIError("Invalid URI"); // Real examples of when they occur: JSON.parse("{invalid}"); // SyntaxError null.toString(); // TypeError console.log(undeclaredVar); // ReferenceError new Array(-1); // RangeError

Custom Error Classes

// Best practice: create custom errors for your application class AppError extends Error { constructor(message, statusCode, code) { super(message); this.name = "AppError"; this.statusCode = statusCode; this.code = code; this.isOperational = true; // Distinguishes from programming errors } } class NotFoundError extends AppError { constructor(resource = "Resource") { super(`${resource} not found`, 404, "NOT_FOUND"); this.name = "NotFoundError"; } } class ValidationError extends AppError { constructor(message, fields = []) { super(message, 400, "VALIDATION_ERROR"); this.name = "ValidationError"; this.fields = fields; } } class UnauthorizedError extends AppError { constructor(message = "Authentication required") { super(message, 401, "UNAUTHORIZED"); this.name = "UnauthorizedError"; } } // Usage: function findUser(id) { const user = database.get(id); if (!user) throw new NotFoundError("User"); return user; } try { const user = findUser("nonexistent"); } catch (error) { if (error instanceof NotFoundError) { console.log(error.statusCode); // 404 console.log(error.code); // "NOT_FOUND" } }

Error Handling Best Practices

// 1. Don't catch errors you can't handle // BAD: try { doSomething(); } catch (e) { // Silently swallowing errors — terrible practice! } // GOOD: try { doSomething(); } catch (e) { logger.error("doSomething failed:", e); throw e; // Re-throw if you can't handle it } // 2. Use specific catches try { const data = JSON.parse(rawData); processData(data); } catch (error) { if (error instanceof SyntaxError) { console.error("Invalid JSON:", error.message); } else { throw error; // Re-throw unexpected errors } } // 3. Centralized error handling (Express pattern — Module 04) // app.use((err, req, res, next) => { // const statusCode = err.statusCode || 500; // res.status(statusCode).json({ // error: err.message, // code: err.code || "INTERNAL_ERROR" // }); // });

20. ES6+ Features You Must Know

Destructuring (Already covered, but here's more)

// Swapping variables let a = 1, b = 2; [a, b] = [b, a]; // a = 2, b = 1 — no temp variable needed! // Skipping elements const [, second, , fourth] = [1, 2, 3, 4]; // second = 2, fourth = 4

Map and Set

// MAP — key-value pairs where keys can be ANY type const map = new Map(); map.set("name", "Alice"); map.set(42, "a number key"); map.set(true, "a boolean key"); const objKey = { id: 1 }; map.set(objKey, "an object key"); // Objects as keys! (impossible with regular objects) map.get("name"); // "Alice" map.has("name"); // true map.delete("name"); // true map.size; // 3 // Iterating for (let [key, value] of map) { console.log(key, value); } // Map vs Object: // - Map: any key type, ordered, has .size, better for frequent add/delete // - Object: string/symbol keys only, not guaranteed order, no .size // SET — unique values only const set = new Set([1, 2, 3, 3, 3]); // {1, 2, 3} — duplicates removed set.add(4); // {1, 2, 3, 4} set.has(3); // true set.delete(3); // {1, 2, 4} set.size; // 3 // Common use: remove duplicates from array const unique = [...new Set([1, 2, 2, 3, 3, 3])]; // [1, 2, 3]

WeakMap and WeakSet

// WeakMap — keys must be objects, and they're held "weakly" // (garbage collected if no other reference exists) const cache = new WeakMap(); function processUser(user) { if (cache.has(user)) { return cache.get(user); // Return cached result } const result = expensiveComputation(user); cache.set(user, result); return result; } let user = { name: "Alice" }; processUser(user); // Computed and cached user = null; // Now the cache entry can be garbage collected! // With a regular Map, the user object would stay in memory forever // WeakSet — same concept, stores objects weakly const visited = new WeakSet(); function trackVisit(user) { visited.add(user); } function hasVisited(user) { return visited.has(user); }

Symbol

// Symbol — creates a unique, immutable identifier const id = Symbol("id"); const anotherId = Symbol("id"); id === anotherId; // false! Every Symbol is unique // Use case 1: Unique object keys (won't conflict with other properties) const user = { name: "Alice", [id]: 12345 // Hidden from for...in, Object.keys(), JSON.stringify() }; user[id]; // 12345 // Use case 2: Well-known Symbols (customize object behavior) class MyArray { *[Symbol.iterator]() { yield 1; yield 2; yield 3; } } for (let val of new MyArray()) { console.log(val); // 1, 2, 3 }

Optional Chaining and Nullish Coalescing

// Optional Chaining (?.) — safely access nested properties const user = { name: "Alice", address: { city: "NYC" } }; user.address?.city; // "NYC" user.phone?.number; // undefined (no error) user.getAddress?.(); // undefined (safely calls if method exists) user.friends?.[0]?.name; // undefined (works with arrays too) // Nullish Coalescing (??) — default only for null/undefined const port = process.env.PORT ?? 3000; const count = 0 ?? 10; // 0 (not null/undefined) const name = null ?? "Anonymous"; // "Anonymous" const text = "" ?? "default"; // "" (empty string is not null/undefined) // Combining both const city = user?.address?.city ?? "Unknown";

21. Practice Problems

Problem 1: Flatten Nested Object

// Write a function that flattens a nested object into dot-notation keys // Input: { a: { b: { c: 1 } }, d: 2 } // Output: { "a.b.c": 1, "d": 2 } // TRY IT YOURSELF FIRST! // Solution: function flatten(obj, prefix = '', result = {}) { for (const key in obj) { const newKey = prefix ? `${prefix}.${key}` : key; if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) { flatten(obj[key], newKey, result); } else { result[newKey] = obj[key]; } } return result; } // Test: console.log(flatten({ a: { b: { c: 1 } }, d: 2, e: { f: 3, g: { h: 4 } } })); // { "a.b.c": 1, "d": 2, "e.f": 3, "e.g.h": 4 }

Problem 2: Debounce Function

// Implement a debounce function. It should delay execution until // the user stops calling it for `delay` milliseconds. // Used in: search autocomplete, window resize handlers // TRY IT YOURSELF FIRST! // Solution: function debounce(fn, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); // Cancel previous timer timeoutId = setTimeout(() => { fn.apply(this, args); }, delay); }; } // Usage: const searchAPI = debounce((query) => { console.log(`Searching for: ${query}`); }, 300); // If user types "hello" quickly: searchAPI("h"); // Timer set searchAPI("he"); // Previous timer cancelled, new timer set searchAPI("hel"); // Previous timer cancelled, new timer set searchAPI("hell"); // Previous timer cancelled, new timer set searchAPI("hello"); // Previous timer cancelled, new timer set // After 300ms of no calls: "Searching for: hello" (only fires once!)

Problem 3: Deep Clone

// Implement a deep clone function that handles objects, arrays, dates, and nested structures // TRY IT YOURSELF FIRST! // Solution: function deepClone(value) { // Handle primitives and null if (value === null || typeof value !== 'object') { return value; } // Handle Date if (value instanceof Date) { return new Date(value.getTime()); } // Handle Array if (Array.isArray(value)) { return value.map(item => deepClone(item)); } // Handle Object const cloned = {}; for (const key in value) { if (Object.hasOwn(value, key)) { cloned[key] = deepClone(value[key]); } } return cloned; } // Test: const original = { name: "Alice", scores: [1, 2, [3, 4]], meta: { created: new Date(), nested: { deep: true } } }; const clone = deepClone(original); clone.scores[2].push(5); console.log(original.scores[2]); // [3, 4] — unaffected!

Problem 4: Implement Array.prototype.reduce from Scratch

// TRY IT YOURSELF FIRST! // Solution: Array.prototype.myReduce = function(callback, initialValue) { let accumulator; let startIndex; if (initialValue !== undefined) { 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++) { accumulator = callback(accumulator, this[i], i, this); } return accumulator; }; // Test: [1, 2, 3, 4].myReduce((sum, n) => sum + n, 0); // 10 [1, 2, 3, 4].myReduce((sum, n) => sum + n); // 10

Problem 5: Event Scheduler with Closures

// Create a scheduler that can schedule functions to run after a delay, // cancel scheduled tasks, and list pending tasks. // TRY IT YOURSELF FIRST! // Solution: function createScheduler() { const tasks = new Map(); let nextId = 1; return { schedule(fn, delayMs, label = "task") { const id = nextId++; const timerId = setTimeout(() => { fn(); tasks.delete(id); }, delayMs); tasks.set(id, { label, timerId, scheduledAt: Date.now(), delayMs }); return id; }, cancel(id) { const task = tasks.get(id); if (!task) return false; clearTimeout(task.timerId); tasks.delete(id); return true; }, pending() { return Array.from(tasks.entries()).map(([id, task]) => ({ id, label: task.label, remainingMs: task.delayMs - (Date.now() - task.scheduledAt) })); }, cancelAll() { for (const [id, task] of tasks) { clearTimeout(task.timerId); } tasks.clear(); } }; } // Usage: const scheduler = createScheduler(); const id1 = scheduler.schedule(() => console.log("Task 1"), 5000, "Email reminder"); const id2 = scheduler.schedule(() => console.log("Task 2"), 10000, "Database cleanup"); console.log(scheduler.pending()); scheduler.cancel(id1); // Cancel task 1

22. Interview Questions

Q1: What is the difference between null and undefined?

Answer: undefined means a variable has been declared but not assigned a value. null is an intentional assignment meaning "no value." typeof undefined is "undefined", but typeof null is "object" (a historic bug). null == undefined is true, but null === undefined is false.

Q2: Explain event delegation.

Answer: Instead of attaching event listeners to each child element, you attach one listener to a parent element and use event.target to determine which child was clicked. This is more memory-efficient and works with dynamically added elements.

Q3: What is the output?

for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); }

Answer: 3, 3, 3. Because var is function-scoped, there's one i shared by all callbacks. By the time setTimeout runs, the loop is done and i is 3. Fix: use let instead of var.

Q4: What is the difference between map() and forEach()?

Answer: map() returns a new array with transformed elements. forEach() returns undefined — it's used for side effects only. Use map() when you need the result, forEach() when you just want to iterate.

Q5: Explain prototypal inheritance.

Answer: In JavaScript, objects can inherit properties from other objects through the prototype chain. When you access a property on an object, JavaScript first looks at the object itself, then its prototype, then the prototype's prototype, until it reaches Object.prototype (whose prototype is null). ES6 classes are syntactic sugar over this prototype system.

Q6: What is the temporal dead zone?

Answer: The TDZ is the period between entering a scope and the actual declaration of a let or const variable. Accessing the variable during this period throws a ReferenceError. This prevents using variables before they're declared, unlike var which returns undefined.

Q7: Implement bind from scratch

Function.prototype.myBind = function(context, ...boundArgs) { const fn = this; return function(...callArgs) { return fn.apply(context, [...boundArgs, ...callArgs]); }; }; function greet(greeting, name) { return `${greeting}, ${this.title} ${name}!`; } const obj = { title: "Dr." }; const greetDr = greet.myBind(obj, "Hello"); greetDr("Alice"); // "Hello, Dr. Alice!"

Q8: What are generators? Give a use case.

Answer: Generators are functions that can be paused and resumed. They use function* syntax and yield keyword. Use cases include: lazy evaluation (processing large datasets without loading everything into memory), implementing iterables, and managing async flow (though async/await is preferred now). Example: generating an infinite sequence of IDs.


Next Module: 02 - JavaScript Advanced — Async programming, event loop, promises, and more.