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
- What is JavaScript?
- Running JavaScript
- Variables — var, let, const
- Data Types
- Type Coercion — JavaScript's Biggest Gotcha
- Operators
- Strings — Deep Dive
- Numbers — Deep Dive
- Control Flow
- Functions — The Heart of JavaScript
- Scope — Where Variables Live
- Hoisting — Why Order Doesn't Always Matter
- Closures — The Most Important Concept
- Objects — Deep Dive
- Arrays — Deep Dive
- The
thisKeyword - Prototypes & The Prototype Chain
- Classes
- Error Handling
- ES6+ Features You Must Know
- Practice Problems
- 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 compiledKey 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
- Open Chrome → Press F12 → Go to "Console" tab
- 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 quit3. 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 changeWhy 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:
varis function-scoped and hoisted withundefined.letandconstare block-scoped and hoisted but in a "temporal dead zone" (TDZ) until their declaration.constprevents 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 uniqueNon-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/undefined5. 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 // 1Explicit 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 verboseShort-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) // falseThe 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-69. 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 010. 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 16Parameters 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"); // trueIIFE — 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-scopedLexical 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 3Non-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); // 4The 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 10Real-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);
// 35016. 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/undefinedRule 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 / new — this 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 binding17. 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; // trueWhy 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; // truePrivate 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 methodReal-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); // RangeErrorCustom 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 = 4Map 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); // 10Problem 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 122. 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.