Think of variables like boxes where you store things. var, let, and const are different types of boxes with different rules about where you can use them and what you can do with them.
// ============= VAR - The Old Way =============
// var is like having a box that can be accessed from anywhere in a function
// Think of it like a public announcement in a building - everyone in that building can hear it
function oldSchoolExample() {
console.log(beforeDeclaration); // This prints "undefined" - weird, right?
// This works because var is "hoisted" - JavaScript moves the declaration to the top
var beforeDeclaration = "I'm a var variable";
// This creates a variable that can be accessed anywhere in this function
if (true) {
var insideBlock = "I can escape this block!";
// Even though this is inside an if statement, var can be used outside too
}
console.log(insideBlock); // This works! Prints "I can escape this block!"
// This is because var ignores block boundaries (like if, for, while)
var canRedeclare = "First value";
var canRedeclare = "Second value"; // This is allowed with var
console.log(canRedeclare); // Prints "Second value"
}
// ============= LET - The Modern Block-Scoped Way =============
// let is like having a box that only works in its specific room (block)
function modernBlockExample() {
// console.log(notHoisted); // This would cause an error!
// let variables are not hoisted like var
let blockScoped = "I respect boundaries";
// This variable can only be used in the current block and its child blocks
if (true) {
let insideBlock = "I'm trapped in this block";
// This variable can ONLY be used inside this if statement
console.log(insideBlock); // This works fine
blockScoped = "I can change the outer variable"; // This works
}
// console.log(insideBlock); // ERROR! This variable doesn't exist here
console.log(blockScoped); // Prints "I can change the outer variable"
// let canRedeclare = "First value";
// let canRedeclare = "Second value"; // ERROR! Can't redeclare with let
}
// ============= CONST - The Unchangeable Way =============
// const is like a safety deposit box - once you put something in, you can't change it
function constantExample() {
const unchangeable = "I can never be reassigned";
// This creates a variable that can never be given a new value
// unchangeable = "New value"; // ERROR! Can't reassign const variables
const person = {
name: "John",
age: 30
};
// Even though person is const, we can still change what's INSIDE the object
person.name = "Jane"; // This is allowed! We're not changing the box, just what's inside
person.age = 25; // This is also allowed
console.log(person); // Prints { name: "Jane", age: 25 }
// person = {}; // ERROR! This would try to put a completely new object in the box
const numbers = [1, 2, 3];
numbers.push(4); // This works! We're modifying the contents, not replacing the array
// numbers = [5, 6, 7]; // ERROR! This would try to replace the entire array
}
// ============= Real-World Comparison =============
function realWorldComparison() {
// VAR is like a megaphone in a building - everyone in the building can hear it
// LET is like talking in a room - only people in that room can hear it
// CONST is like writing something in permanent ink - you can't erase it
for (var i = 0; i < 3; i++) {
// var i can be accessed outside this loop
setTimeout(() => console.log("var:", i), 100); // Prints 3, 3, 3 (unexpected!)
}
for (let j = 0; j < 3; j++) {
// let j is trapped inside each loop iteration
setTimeout(() => console.log("let:", j), 200); // Prints 0, 1, 2 (expected!)
}
}Think of these as different machines in a factory assembly line. Each machine does a specific job with the items that come through.
// ============= MAP - The Transformer Machine =============
// map is like a machine that takes each item, does something to it, and puts out a new item
// IMPORTANT: map ALWAYS returns an array with the SAME number of items
const numbers = [1, 2, 3, 4, 5];
// Basic transformation - multiply each number by 2
const doubled = numbers.map(function(currentNumber) {
// This function runs once for each item in the array
// currentNumber will be 1, then 2, then 3, etc.
console.log("Processing:", currentNumber);
return currentNumber * 2; // Whatever we return becomes the new value
});
console.log("Original:", numbers); // [1, 2, 3, 4, 5] - unchanged!
console.log("Doubled:", doubled); // [2, 4, 6, 8, 10] - new array!
// Real-world example - converting temperatures
const celsiusTemps = [0, 20, 30, 40];
const fahrenheitTemps = celsiusTemps.map(function(celsius) {
// Formula to convert Celsius to Fahrenheit: (C × 9/5) + 32
const fahrenheit = (celsius * 9/5) + 32;
return fahrenheit;
});
console.log("Celsius:", celsiusTemps); // [0, 20, 30, 40]
console.log("Fahrenheit:", fahrenheitTemps); // [32, 68, 86, 104]
// Working with objects - adding sales tax to prices
const products = [
{ name: "Laptop", price: 1000 },
{ name: "Mouse", price: 25 },
{ name: "Keyboard", price: 75 }
];
const productsWithTax = products.map(function(product) {
// We're creating a completely new object for each product
return {
name: product.name, // Keep the same name
originalPrice: product.price, // Store the original price
priceWithTax: product.price * 1.08 // Add 8% tax
};
});
console.log(productsWithTax);
// ============= FILTER - The Selector Machine =============
// filter is like a quality control machine that only lets certain items through
// IMPORTANT: filter returns an array with FEWER (or same) number of items
const ages = [12, 18, 21, 15, 30, 16, 25];
// Find all adults (18 or older)
const adults = ages.filter(function(age) {
// This function must return true or false
// If true, this item goes into the new array
// If false, this item gets thrown away
console.log("Checking age:", age);
if (age >= 18) {
console.log(" -> Adult! Including in result");
return true; // Include this age
} else {
console.log(" -> Minor! Excluding from result");
return false; // Don't include this age
}
});
console.log("All ages:", ages); // [12, 18, 21, 15, 30, 16, 25]
console.log("Adult ages:", adults); // [18, 21, 30, 25]
// Real-world example - finding expensive products
const inventory = [
{ name: "Pencil", price: 1, inStock: true },
{ name: "Notebook", price: 5, inStock: false },
{ name: "Laptop", price: 1200, inStock: true },
{ name: "Phone", price: 800, inStock: true },
{ name: "Tablet", price: 600, inStock: false }
];
const expensiveAvailableItems = inventory.filter(function(item) {
// We can use multiple conditions with && (AND) or || (OR)
const isExpensive = item.price > 100; // Check if price is over $100
const isAvailable = item.inStock; // Check if it's in stock
// Only include items that are BOTH expensive AND available
return isExpensive && isAvailable;
});
console.log(expensiveAvailableItems); // Only laptop and phone
// ============= REDUCE - The Accumulator Machine =============
// reduce is like a machine that takes all items and combines them into ONE final result
// Think of it like a blender - many ingredients go in, one smoothie comes out
const prices = [10, 25, 30, 15, 50];
// Calculate total - most common use of reduce
const total = prices.reduce(function(accumulator, currentPrice) {
// accumulator = the running total so far
// currentPrice = the current item we're processing
console.log("Current total:", accumulator, "+ Current price:", currentPrice);
const newTotal = accumulator + currentPrice;
console.log("New total:", newTotal);
return newTotal; // This becomes the accumulator for the next iteration
}, 0); // 0 is the starting value for accumulator
console.log("Final total:", total); // 130
// Advanced example - counting occurrences
const fruits = ["apple", "banana", "apple", "orange", "banana", "apple"];
const fruitCount = fruits.reduce(function(counter, currentFruit) {
// counter is an object that keeps track of how many of each fruit we've seen
// currentFruit is the fruit we're currently looking at
console.log("Processing:", currentFruit);
console.log("Current counter:", counter);
if (counter[currentFruit]) {
// If we've seen this fruit before, add 1 to its count
counter[currentFruit] = counter[currentFruit] + 1;
} else {
// If this is the first time seeing this fruit, set count to 1
counter[currentFruit] = 1;
}
console.log("Updated counter:", counter);
return counter; // Return the updated counter for next iteration
}, {}); // Start with an empty object
console.log("Final count:", fruitCount); // { apple: 3, banana: 2, orange: 1 }
// ============= CHAINING - Using Multiple Methods Together =============
// You can chain these methods together like an assembly line
const studentGrades = [85, 92, 78, 96, 88, 73, 91];
const result = studentGrades
.filter(function(grade) {
// Step 1: Only keep passing grades (75 or higher)
console.log("Filtering grade:", grade);
return grade >= 75;
})
.map(function(grade) {
// Step 2: Convert each grade to a letter grade
console.log("Converting grade:", grade);
if (grade >= 90) return "A";
if (grade >= 80) return "B";
if (grade >= 70) return "C";
return "F";
})
.reduce(function(counter, letterGrade) {
// Step 3: Count how many of each letter grade
console.log("Counting letter grade:", letterGrade);
counter[letterGrade] = (counter[letterGrade] || 0) + 1;
return counter;
}, {});
console.log("Grade distribution:", result); // { A: 3, B: 3, C: 1 }Functions are like recipes. You can write recipes in different ways, but they all tell you how to make something.
// ============= FUNCTION DECLARATION - The Traditional Recipe =============
// This is like writing a recipe in a cookbook that everyone can find
// Function declarations are "hoisted" - they get moved to the top of their scope
console.log("Can call before declaration:", add(5, 3)); // This works! Prints 8
function add(firstNumber, secondNumber) {
// This is a named function that can be called from anywhere in its scope
// firstNumber and secondNumber are called "parameters" - they're like placeholders
console.log("Adding", firstNumber, "and", secondNumber);
const result = firstNumber + secondNumber; // Do the calculation
return result; // Send the result back to whoever called this function
// Any code after 'return' won't run - return exits the function immediately
console.log("This will never print");
}
// Calling the function with "arguments" (actual values)
const sum = add(10, 20); // 10 and 20 are arguments
console.log("The sum is:", sum); // Prints 30
// ============= FUNCTION EXPRESSION - The Variable Recipe =============
// This is like writing a recipe on a piece of paper and putting it in a box
// console.log(multiply(2, 4)); // ERROR! Can't use before declaration
const multiply = function(x, y) {
// This function doesn't have a name (it's "anonymous")
// It's stored in a variable called 'multiply'
console.log("Multiplying", x, "by", y);
return x * y;
};
console.log("Multiplication result:", multiply(3, 7)); // Now this works
// You can pass function expressions as arguments to other functions
function doMath(mathFunction, a, b) {
console.log("About to do some math...");
const result = mathFunction(a, b); // Call whatever function was passed in
console.log("Math complete! Result:", result);
return result;
}
doMath(multiply, 4, 5); // Pass the multiply function as an argument
// ============= ARROW FUNCTIONS - The Short Recipe =============
// This is like writing a recipe in shorthand notation
// Long form arrow function
const divide = (dividend, divisor) => {
console.log("Dividing", dividend, "by", divisor);
if (divisor === 0) {
console.log("Can't divide by zero!");
return "Error";
}
return dividend / divisor;
};
// Short form arrow function (for simple operations)
const square = (number) => {
return number * number;
};
// Even shorter form (when there's only one expression)
const cube = number => number * number * number;
// This automatically returns the result of number * number * number
// Super short form for simple transformations
const double = x => x * 2;
console.log("Square of 5:", square(5)); // 25
console.log("Cube of 3:", cube(3)); // 27
console.log("Double of 8:", double(8)); // 16
// ============= FUNCTIONS WITH DEFAULT PARAMETERS =============
// Like having backup ingredients in case someone forgets to bring something
function greetPerson(name = "Friend", greeting = "Hello") {
// If name isn't provided, it defaults to "Friend"
// If greeting isn't provided, it defaults to "Hello"
console.log("Name provided:", name);
console.log("Greeting provided:", greeting);
return `${greeting}, ${name}! How are you today?`;
}
console.log(greetPerson()); // Uses both defaults
console.log(greetPerson("Alice")); // Uses default greeting
console.log(greetPerson("Bob", "Hi there")); // Uses provided values
// ============= FUNCTIONS WITH REST PARAMETERS =============
// Like a recipe that can handle any number of ingredients
function calculateAverage(...numbers) {
// The ...numbers collects all arguments into an array
console.log("Numbers received:", numbers);
console.log("How many numbers:", numbers.length);
if (numbers.length === 0) {
console.log("No numbers provided!");
return 0;
}
// Use reduce to add all numbers together
const sum = numbers.reduce((total, current) => {
console.log("Adding", current, "to", total);
return total + current;
}, 0);
const average = sum / numbers.length;
console.log("Sum:", sum, "Count:", numbers.length, "Average:", average);
return average;
}
console.log("Average of 2, 4, 6:", calculateAverage(2, 4, 6)); // 4
console.log("Average of 10, 20:", calculateAverage(10, 20)); // 15
console.log("Average of 1, 2, 3, 4, 5:", calculateAverage(1, 2, 3, 4, 5)); // 3
// ============= IMMEDIATELY INVOKED FUNCTION EXPRESSION (IIFE) =============
// Like cooking and eating a meal immediately without saving the recipe
(function() {
// This function runs immediately when JavaScript reads it
console.log("I run immediately!");
const secretVariable = "This can't be accessed from outside";
console.log("Secret:", secretVariable);
// Any variables declared here are private to this function
})(); // The () at the end calls the function immediately
// console.log(secretVariable); // ERROR! This variable doesn't exist outside the IIFE
// IIFE with parameters
(function(message, times) {
for (let i = 0; i < times; i++) {
console.log(`${i + 1}: ${message}`);
}
})("Hello World!", 3); // Pass arguments to the IIFE
// ============= FUNCTION SCOPE AND VARIABLE ACCESS =============
// Functions create their own private space for variables
const globalVariable = "I'm available everywhere";
function outerFunction(outerParam) {
const outerVariable = "I'm in the outer function";
console.log("Outer can access global:", globalVariable);
console.log("Outer parameter:", outerParam);
function innerFunction(innerParam) {
const innerVariable = "I'm in the inner function";
// Inner function can access everything from outer scopes
console.log("Inner can access global:", globalVariable);
console.log("Inner can access outer variable:", outerVariable);
console.log("Inner can access outer parameter:", outerParam);
console.log("Inner parameter:", innerParam);
console.log("Inner variable:", innerVariable);
}
innerFunction("inner argument");
// console.log(innerVariable); // ERROR! Outer can't access inner variables
}
outerFunction("outer argument");A closure is like a backpack that a function carries around. The backpack contains all the variables from the place where the function was created, and the function can always look in its backpack to find those variables, even when it's used somewhere else.
// ============= BASIC CLOSURE EXAMPLE =============
// The simplest way to understand closures
function createCounter() {
// This variable lives inside createCounter
let count = 0;
console.log("Creating a counter, starting count is:", count);
// We're returning a function that "remembers" the count variable
return function() {
count = count + 1; // The inner function can access and modify count
console.log("Count is now:", count);
return count;
};
}
// Create two separate counters
const counter1 = createCounter(); // This creates the first backpack with count = 0
const counter2 = createCounter(); // This creates a second, separate backpack with count = 0
console.log("Using counter1:");
counter1(); // Prints "Count is now: 1"
counter1(); // Prints "Count is now: 2"
counter1(); // Prints "Count is now: 3"
console.log("Using counter2:");
counter2(); // Prints "Count is now: 1" (separate from counter1!)
counter2(); // Prints "Count is now: 2"
console.log("Back to counter1:");
counter1(); // Prints "Count is now: 4" (remembered where it left off!)
// ============= CLOSURE WITH PARAMETERS =============
// Creating customized functions that remember their setup
function createMultiplier(multiplier) {
console.log("Creating a multiplier function for:", multiplier);
// The returned function "closes over" the multiplier parameter
return function(number) {
console.log(`Multiplying ${number} by ${multiplier}`);
return number * multiplier;
};
}
const double = createMultiplier(2); // Creates a function that multiplies by 2
const triple = createMultiplier(3); // Creates a function that multiplies by 3
const times10 = createMultiplier(10); // Creates a function that multiplies by 10
console.log("2 doubled:", double(2)); // 4
console.log("5 tripled:", triple(5)); // 15
console.log("7 times 10:", times10(7)); // 70
// Each function remembers its own multiplier value!
// ============= PRACTICAL EXAMPLE - PRIVATE VARIABLES =============
// Using closures to create "private" variables that can't be accessed directly
function createBankAccount(initialBalance) {
// These variables are "private" - they can't be accessed from outside
let balance = initialBalance;
let transactionHistory = [];
console.log("Creating bank account with balance:", balance);
// Return an object with methods that can access the private variables
return {
// Method to check balance
getBalance: function() {
console.log("Checking balance...");
return balance;
},
// Method to deposit money
deposit: function(amount) {
if (amount > 0) {
balance = balance + amount; // Access the private balance variable
const transaction = `Deposited $${amount}`;
transactionHistory.push(transaction); // Access private transaction history
console.log(transaction, "- New balance:", balance);
return balance;
} else {
console.log("Deposit amount must be positive");
return balance;
}
},
// Method to withdraw money
withdraw: function(amount) {
if (amount > 0 && amount <= balance) {
balance = balance - amount; // Modify the private balance
const transaction = `Withdrew $${amount}`;
transactionHistory.push(transaction);
console.log(transaction, "- New balance:", balance);
return balance;
} else {
console.log("Invalid withdrawal amount");
return balance;
}
},
// Method to get transaction history
getHistory: function() {
console.log("Transaction history:");
transactionHistory.forEach((transaction, index) => {
console.log(`${index + 1}: ${transaction}`);
});
return [...transactionHistory]; // Return a copy, not the original array
}
};
}
const myAccount = createBankAccount(100);
console.log("Initial balance:", myAccount.getBalance()); // 100
myAccount.deposit(50); // Balance becomes 150
myAccount.withdraw(30); // Balance becomes 120
myAccount.withdraw(200); // Should fail - insufficient funds
myAccount.getHistory(); // Shows all transactions
// Try to access private variables directly - this won't work!
// console.log(myAccount.balance); // undefined - can't access private variable!
// ============= CLOSURE IN LOOPS - COMMON GOTCHA =============
// This is a very common interview question about closures
console.log("=== CLOSURE LOOP PROBLEMS ===");
// WRONG WAY - This doesn't work as expected
console.log("Wrong way with var:");
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log("var loop - i is:", i); // This prints 3, 3, 3 (not what we want!)
}, 100);
}
// Why? Because var doesn't create a new scope for each iteration
// All the functions share the same 'i' variable, and by the time they run, i = 3
// RIGHT WAY 1 - Using let instead of var
console.log("Right way with let:");
for (let j = 0; j < 3; j++) {
setTimeout(function() {
console.log("let loop - j is:", j); // This prints 0, 1, 2 (what we want!)
}, 200);
}
// Why? Because let creates a new scope for each iteration
// Each function gets its own copy of j
// RIGHT WAY 2 - Using closure to capture the value
console.log("Right way with closure:");
for (var k = 0; k < 3; k++) {
// Create a closure that captures the current value of k
(function(capturedK) {
setTimeout(function() {
console.log("closure loop - capturedK is:", capturedK); // Prints 0, 1, 2
}, 300);
})(k); // Pass the current value of k to the closure
}
// ============= ADVANCED CLOSURE EXAMPLE - MODULE PATTERN =============
// Creating a module with public and private methods
const CalculatorModule = (function() {
// Private variables and functions
let history = [];
let currentResult = 0;
function addToHistory(operation, result) {
const entry = `${operation} = ${result}`;
history.push(entry);
console.log("Added to history:", entry);
}
function validateNumber(num) {
if (typeof num !== 'number' || isNaN(num)) {
throw new Error("Please provide a valid number");
}
}
// Return public interface
return {
// Public method to add
add: function(number) {
validateNumber(number);
currentResult += number;
addToHistory(`+ ${number}`, currentResult);
return this; // Return 'this' to allow method chaining
},
// Public method to subtract
subtract: function(number) {
validateNumber(number);
currentResult -= number;
addToHistory(`- ${number}`, currentResult);
return this; // Allow chaining
},
// Public method to multiply
multiply: function(number) {
validateNumber(number);
currentResult *= number;
addToHistory(`* ${number}`, currentResult);
return this; // Allow chaining
},
// Public method to get current result
getResult: function() {
console.log("Current result:", currentResult);
return currentResult;
},
// Public method to get history
getHistory: function() {
console.log("Calculation history:");
history.forEach((entry, index) => {
console.log(`${index + 1}: ${entry}`);
});
return [...history]; // Return copy of history
},
// Public method to reset
reset: function() {
currentResult = 0;
history = [];
console.log("Calculator reset");
return this; // Allow chaining
}
};
})(); // IIFE - runs immediately and returns the public interface
// Using the calculator module
CalculatorModule
.add(10) // 10
.multiply(3) // 30
.subtract(5) // 25
.add(2) // 27
.getResult(); // Shows 27
CalculatorModule.getHistory(); // Shows all operations
// Try to access private variables - won't work!
// console.log(CalculatorModule.history); // undefined
// console.log(CalculatorModule.currentResult); // undefinedCurrying is like having a recipe that you can prepare in stages. Instead of adding all ingredients at once, you add them one at a time, and each step gives you a new recipe that's waiting for the next ingredient.
// ============= BASIC CURRYING CONCEPT =============
// Regular function vs curried function
// REGULAR FUNCTION - Takes all arguments at once
function regularAdd(a, b, c) {
console.log(`Adding ${a} + ${b} + ${c}`);
return a + b + c;
}
const result1 = regularAdd(1, 2, 3); // Must provide all arguments at once
console.log("Regular result:", result1); // 6
// CURRIED FUNCTION - Takes one argument at a time
function curriedAdd(a) {
console.log("First function received:", a);
return function(b) {
console.log("Second function received:", b);
return function(c) {
console.log("Third function received:", c);
console.log(`Final calculation: ${a} + ${b} + ${c}`);
return a + b + c;
};
};
}
// Using the curried function - step by step
const step1 = curriedAdd(1); // Returns a function waiting for 'b'
const step2 = step1(2); // Returns a function waiting for 'c'
const result2 = step2(3); // Finally calculates the result
console.log("Curried result:", result2); // 6
// Or use it all at once with multiple parentheses
const result3 = curriedAdd(10)(20)(30);
console.log("Chained curried result:", result3); // 60
// ============= ARROW FUNCTION CURRYING (SHORTER SYNTAX) =============
// Much cleaner way to write curried functions
const curriedMultiply = a => b => c => {
console.log(`Multiplying ${a} × ${b} × ${c}`);
return a * b * c;
};
// Each arrow function takes one parameter and returns the next function
// a => (returns a function that takes b)
// b => (returns a function that takes c)
// c => (returns the final result)
const double = curriedMultiply(2); // Partially applied - waiting for b and c
const doubleByFive = double(5); // More specific - waiting for c
const finalResult = doubleByFive(3); // Complete calculation: 2 × 5 × 3 = 30
console.log("Arrow curried result:", finalResult);
// ============= PRACTICAL EXAMPLE - CUSTOMIZED GREETING FUNCTION =============
// Creating specialized greeting functions
const createGreeting = greeting => name => timeOfDay => {
const message = `${greeting}, ${name}! Have a wonderful ${timeOfDay}!`;
console.log("Generated greeting:", message);
return message;
};
// Create specialized greeting functions
const sayHello = createGreeting("Hello"); // Partially applied
const sayGoodMorning = sayHello("Alice"); // More specific
const morningGreetingForAlice = sayGoodMorning("morning"); // Complete
// Create different specialized versions
const casualGreeting = createGreeting("Hey");
const formalGreeting = createGreeting("Good day");
const greetBob = casualGreeting("Bob");
const greetDrSmith = formalGreeting("Dr. Smith");
console.log(greetBob("evening")); // "Hey, Bob! Have a wonderful evening!"
console.log(greetDrSmith("afternoon")); // "Good day, Dr. Smith! Have a wonderful afternoon!"
// ============= CURRYING FOR CONFIGURATION =============
// Using currying to create configurable functions
const createValidator = errorMessage => validationFunction => value => {
console.log(`Validating value: ${value}`);
console.log(`Using validation: ${validationFunction.name || 'custom function'}`);
const isValid = validationFunction(value);
if (isValid) {
console.log("✓ Validation passed");
return { isValid: true, value: value };
} else {
console.log("✗ Validation failed:", errorMessage);
return { isValid: false, error: errorMessage };
}
};
// Create specific validators
const createNumberValidator = createValidator("Must be a number");
const createStringValidator = createValidator("Must be a non-empty string");
const createEmailValidator = createValidator("Must be a valid email");
// Define validation functions
const isNumber = value => typeof value === 'number' && !isNaN(value);
const isNonEmptyString = value => typeof value === 'string' && value.length > 0;
const isEmail = value => typeof value === 'string' && value.includes('@') && value.includes('.');
// Create specific validator functions
const validateNumber = createNumberValidator(isNumber);
const validateString = createStringValidator(isNonEmptyString);
const validateEmail = createEmailValidator(isEmail);
// Test the validators
console.log("=== Testing Validators ===");
console.log(validateNumber(42)); // Valid
console.log(validateNumber("not a number")); // Invalid
console.log(validateString("Hello")); // Valid
console.log(validateString("")); // Invalid
console.log(validateEmail("test@email.com")); // Valid
console.log(validateEmail("not-an-email")); // Invalid
// ============= CURRYING WITH ARRAY OPERATIONS =============
// Creating reusable array processing functions
const createArrayProcessor = operation => array => {
console.log(`Processing array with ${operation.name}:`, array);
const result = array.map(operation);
console.log("Result:", result);
return result;
};
// Define operations
const addTax = price => {
const withTax = price * 1.08;
console.log(` ${price} + tax = ${withTax.toFixed(2)}`);
return withTax;
};
const applyDiscount = price => {
const discounted = price * 0.9;
console.log(` ${price} - 10% = ${discounted.toFixed(2)}`);
return discounted;
};
const formatCurrency = price => {
const formatted = `$${price.toFixed(2)}`;
console.log(` ${price} formatted = ${formatted}`);
return formatted;
};
// Create specialized array processors
const addTaxToArray = createArrayProcessor(addTax);
const applyDiscountToArray = createArrayProcessor(applyDiscount);
const formatPriceArray = createArrayProcessor(formatCurrency);
const prices = [10, 25, 50, 100];
console.log("=== Processing Price Array ===");
const pricesWithTax = addTaxToArray(prices);
const discountedPrices = applyDiscountToArray(pricesWithTax);
const formattedPrices = formatPriceArray(discountedPrices);
// ============= AUTOMATIC CURRYING HELPER FUNCTION =============
// A function that can convert any regular function into a curried version
function curry(func) {
// Get the number of parameters the original function expects
const arity = func.length;
console.log(`Currying function that expects ${arity} arguments`);
return function curried(...args) {
console.log(`Curried function called with ${args.length} arguments:`, args);
if (args.length >= arity) {
// If we have enough arguments, call the original function
console.log("Enough arguments provided, calling original function");
return func.apply(this, args);
} else {
// If we need more arguments, return a new function that waits for them
console.log(`Need ${arity - args.length} more arguments`);
return function(...nextArgs) {
console.log("Additional arguments provided:", nextArgs);
// Combine the previous arguments with the new ones and try again
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
// Convert a regular function to curried
function regularCalculation(a, b, c, d) {
console.log(`Calculating: (${a} + ${b}) × (${c} + ${d})`);
const result = (a + b) * (c + d);
console.log("Calculation result:", result);
return result;
}
const curriedCalculation = curry(regularCalculation);
// Use the curried version in different ways
console.log("=== Using Auto-Curried Function ===");
// All at once
const result4 = curriedCalculation(1, 2, 3, 4); // (1+2) × (3+4) = 21
// Partially applied
const partialCalc = curriedCalculation(1, 2); // Waiting for 2 more args
const morePartialCalc = partialCalc(3); // Waiting for 1 more arg
const finalCalc = morePartialCalc(4); // Complete calculation
// Mixed approach
const mixedResult = curriedCalculation(10)(20, 30)(40); // (10+20) × (30+40) = 2100
// ============= REAL-WORLD EXAMPLE - HTTP REQUEST BUILDER =============
// Using currying to build HTTP requests step by step
const createHttpRequest = method => url => headers => body => {
const request = {
method: method.toUpperCase(),
url: url,
headers: headers || {},
body: body || null
};
console.log("HTTP Request created:");
console.log("Method:", request.method);
console.log("URL:", request.url);
console.log("Headers:", request.headers);
console.log("Body:", request.body);
// In a real application, you would make the actual HTTP request here
return request;
};
// Create method-specific request builders
const createGetRequest = createHttpRequest("GET");
const createPostRequest = createHttpRequest("POST");
const createPutRequest = createHttpRequest("PUT");
// Create URL-specific builders
const getFromApi = createGetRequest("https://api.example.com");
const postToApi = createPostRequest("https://api.example.com");
// Create endpoint-specific builders
const getUsersRequest = getFromApi({ "Authorization": "Bearer token123" });
const createUserRequest = postToApi({ "Content-Type": "application/json" });
// Make specific requests
const getUsersCall = getUsersRequest(null); // GET requests don't need a body
const createUserCall = createUserRequest(JSON.stringify({ name: "John", email: "john@example.com" }));Objects in JavaScript are like containers that hold related information and functionality together. Think of an object like a filing cabinet where each drawer (property) has a label and contains something useful.
// ============= BASIC OBJECT CREATION AND ACCESS =============
// Different ways to create objects
// Method 1: Object Literal (most common)
const person = {
// Properties (data about the object)
firstName: "John", // String property
lastName: "Doe", // String property
age: 30, // Number property
isEmployed: true, // Boolean property
hobbies: ["reading", "swimming", "coding"], // Array property
// Methods (functions that belong to the object)
getFullName: function() {
// 'this' refers to the object this method belongs to
console.log("Getting full name for:", this.firstName);
return this.firstName + " " + this.lastName;
},
// Shorter method syntax (ES6)
introduce() {
const fullName = this.getFullName(); // Call another method
console.log(`Hi, I'm ${fullName} and I'm ${this.age} years old.`);
return `Hello from ${fullName}`;
},
// Method that modifies the object
haveBirthday() {
console.log(`${this.firstName} is having a birthday!`);
this.age = this.age + 1; // Modify the age property
console.log(`${this.firstName} is now ${this.age} years old.`);
}
};
// Accessing object properties
console.log("=== Accessing Object Properties ===");
console.log("First name:", person.firstName); // Dot notation
console.log("Last name:", person["lastName"]); // Bracket notation
console.log("Age:", person.age);
// Calling object methods
console.log("Full name:", person.getFullName());
person.introduce();
person.haveBirthday();
// ============= DYNAMIC PROPERTY ACCESS =============
// Using variables to access properties
const propertyName = "hobbies";
console.log("Dynamic access:", person[propertyName]); // Access using variable
// Adding new properties dynamically
person.occupation = "Software Developer"; // Add new property
person["favoriteColor"] = "blue"; // Add using bracket notation
console.log("Added occupation:", person.occupation);
console.log("Added color:", person.favoriteColor);
// ============= OBJECT CONSTRUCTOR FUNCTION =============
// Creating multiple similar objects
function Car(make, model, year) {
// 'this' refers to the new object being created
console.log("Creating a new car...");
this.make = make;
this.model = model;
this.year = year;
this.isRunning = false;
this.mileage = 0;
// Methods added to each instance
this.start = function() {
if (!this.isRunning) {
this.isRunning = true;
console.log(`${this.year} ${this.make} ${this.model} is now running!`);
} else {
console.log("Car is already running!");
}
};
this.stop = function() {
if (this.isRunning) {
this.isRunning = false;
console.log(`${this.year} ${this.make} ${this.model} has stopped.`);
} else {
console.log("Car is already stopped!");
}
};
this.drive = function(miles) {
if (this.isRunning) {
this.mileage += miles;
console.log(`Drove ${miles} miles. Total mileage: ${this.mileage}`);
} else {
console.log("Can't drive! Car is not running. Please start the car first.");
}
};
this.getInfo = function() {
const status = this.isRunning ? "running" : "stopped";
return `${this.year} ${this.make} ${this.model} - ${this.mileage} miles - ${status}`;
};
}
// Creating new car objects
console.log("=== Creating Cars ===");
const car1 = new Car("Toyota", "Camry", 2020);
const car2 = new Car("Honda", "Civic", 2019);
console.log("Car 1:", car1.getInfo());
console.log("Car 2:", car2.getInfo());
car1.start();
car1.drive(100);
car1.stop();
car2.start();
car2.drive(50);
console.log("Car 2 after driving:", car2.getInfo());
// ============= OBJECT.CREATE() METHOD =============
// Creating objects with a specific prototype
const animalPrototype = {
// Shared methods for all animals
eat: function(food) {
console.log(`${this.name} is eating ${food}`);
this.energy = (this.energy || 50) + 10;
console.log(`${this.name}'s energy is now ${this.energy}`);
},
sleep: function(hours) {
console.log(`${this.name} is sleeping for ${hours} hours`);
this.energy = (this.energy || 50) + (hours * 5);
console.log(`${this.name} feels refreshed! Energy: ${this.energy}`);
},
makeSound: function() {
const sound = this.sound || "some sound";
console.log(`${this.name} makes ${sound}`);
}
};
// Create specific animals using the prototype
const dog = Object.create(animalPrototype);
dog.name = "Buddy";
dog.species = "Dog";
dog.sound = "woof woof";
dog.energy = 60;
const cat = Object.create(animalPrototype);
cat.name = "Whiskers";
cat.species = "Cat";
cat.sound = "meow";
cat.energy = 40;
console.log("=== Animals with Shared Behavior ===");
dog.makeSound();
dog.eat("dog food");
dog.sleep(8);
cat.makeSound();
cat.eat("fish");
cat.sleep(12);
// ============= OBJECT DESTRUCTURING =============
// Extracting properties from objects into variables
const student = {
name: "Alice",
age: 22,
major: "Computer Science",
gpa: 3.8,
address: {
street: "123 Main St",
city: "Boston",
state: "MA"
},
courses: ["Math", "Physics", "Programming"]
};
console.log("=== Object Destructuring ===");
// Basic destructuring
const { name, age, major } = student;
console.log("Extracted:", name, age, major);
// Destructuring with renaming
const { gpa: gradePointAverage, courses: classList } = student;
console.log("GPA (renamed):", gradePointAverage);
console.log("Classes (renamed):", classList);
// Nested destructuring
const { address: { city, state } } = student;
console.log("Location:", city, state);
// Destructuring with default values
const { graduation = "2025", scholarship = false } = student;
console.log("Graduation year:", graduation); // Uses default
console.log("Has scholarship:", scholarship); // Uses default
// ============= OBJECT METHODS AND PROPERTY MANIPULATION =============
// Built-in methods for working with objects
const inventory = {
apples: 50,
bananas: 30,
oranges: 25,
grapes: 40
};
console.log("=== Object Methods ===");
// Object.keys() - Get all property names
const itemNames = Object.keys(inventory);
console.log("Items in inventory:", itemNames);
// Object.values() - Get all property values
const quantities = Object.values(inventory);
console.log("Quantities:", quantities);
// Object.entries() - Get key-value pairs
const inventoryEntries = Object.entries(inventory);
console.log("Key-value pairs:", inventoryEntries);
// Calculate total inventory using Object.values()
const totalItems = quantities.reduce((total, quantity) => {
console.log(`Adding ${quantity} to total ${total}`);
return total + quantity;
}, 0);
console.log("Total items in inventory:", totalItems);
// Object.assign() - Copy properties from one object to another
const newStock = { pears: 20, peaches: 15 };
const updatedInventory = Object.assign({}, inventory, newStock);
console.log("Updated inventory:", updatedInventory);
// ============= OBJECT PROPERTY DESCRIPTORS =============
// Controlling how properties behave
const secureObject = {};
// Define a property with specific characteristics
Object.defineProperty(secureObject, 'secret', {
value: "top secret data",
writable: false, // Can't be changed
enumerable: false, // Won't show up in for...in loops
configurable: false // Can't be deleted or reconfigured
});
Object.defineProperty(secureObject, 'publicData', {
value: "everyone can see this",
writable: true, // Can be changed
enumerable: true, // Shows up in loops
configurable: true // Can be deleted
});
console.log("=== Property Descriptors ===");
console.log("Secret value:", secureObject.secret);
console.log("Public value:", secureObject.publicData);
// Try to modify the secret (won't work)
secureObject.secret = "trying to change secret";
console.log("Secret after trying to change:", secureObject.secret); // Still original value
// Modify public data (will work)
secureObject.publicData = "modified public data";
console.log("Public after change:", secureObject.publicData);
// See which properties are enumerable
console.log("Enumerable properties:", Object.keys(secureObject)); // Only shows publicData
// ============= OBJECT COMPOSITION AND MIXINS =============
// Combining functionality from multiple objects
const canWalk = {
walk() {
console.log(`${this.name} is walking`);
}
};
const canFly = {
fly() {
console.log(`${this.name} is flying high!`);
}
};
const canSwim = {
swim() {
console.log(`${this.name} is swimming gracefully`);
}
};
// Create a duck that can do all three
function createDuck(name) {
const duck = { name: name };
// Assign multiple behaviors to the duck
Object.assign(duck, canWalk, canFly, canSwim);
// Add duck-specific behavior
duck.quack = function() {
console.log(`${this.name} says: Quack quack!`);
};
return duck;
}
// Create a fish that can only swim
function createFish(name) {
const fish = { name: name };
Object.assign(fish, canSwim);
fish.blowBubbles = function() {
console.log(`${this.name} is blowing bubbles`);
};
return fish;
}
console.log("=== Object Composition ===");
const duck = createDuck("Donald");
const fish = createFish("Nemo");
duck.walk();
duck.fly();
duck.swim();
duck.quack();
fish.swim();
fish.blowBubbles();
// fish.fly(); // This would cause an error - fish can't fly!
// ============= CHECKING OBJECT PROPERTIES =============
// Different ways to check if properties exist
const testObject = {
name: "Test",
value: 0, // Falsy value
data: null, // Null value
info: undefined // Undefined value
};
console.log("=== Property Checking ===");
// hasOwnProperty() - checks if object has the property directly
console.log("Has 'name' property:", testObject.hasOwnProperty('name')); // true
console.log("Has 'missing' property:", testObject.hasOwnProperty('missing')); // false
// 'in' operator - checks if property exists (including inherited)
console.log("'name' in object:", 'name' in testObject); // true
console.log("'toString' in object:", 'toString' in testObject); // true (inherited)
// typeof check - checks if property is not undefined
console.log("name is not undefined:", typeof testObject.name !== 'undefined'); // true
console.log("missing is not undefined:", typeof testObject.missing !== 'undefined'); // false
// Direct comparison with undefined
console.log("value !== undefined:", testObject.value !== undefined); // true (even though value is 0)
console.log("data !== undefined:", testObject.data !== undefined); // true (even though data is null)
console.log("info !== undefined:", testObject.info !== undefined); // falsehasOwnProperty(), in operator, or check !== undefined.undefined rather than throwing an error.Object.assign() and the spread operator? A: Both create shallow copies, but spread operator is more modern and readable.The this keyword in JavaScript is like a chameleon - it changes based on how and where a function is called. Think of this as asking "who am I working for right now?" The answer depends on the situation.
// ============= GLOBAL CONTEXT - DEFAULT BINDING =============
// When 'this' is used outside any object or function
console.log("=== Global Context ===");
console.log("Global this:", this); // In browsers: window object, in Node.js: global object
function globalFunction() {
console.log("Inside globalFunction, this is:", this);
// In non-strict mode: points to global object (window/global)
// In strict mode: this would be undefined
}
globalFunction(); // Called without any object context
// ============= IMPLICIT BINDING - OBJECT METHOD CALLS =============
// When a function is called as a method of an object
const restaurant = {
name: "Mario's Pizza",
location: "New York",
rating: 4.5,
// Method where 'this' refers to the restaurant object
getInfo: function() {
console.log("=== Restaurant Info ===");
console.log("In getInfo, 'this' refers to:", this);
console.log(`Restaurant: ${this.name}`);
console.log(`Location: ${this.location}`);
console.log(`Rating: ${this.rating} stars`);
return `${this.name} in ${this.location}`;
},
// Method that calls another method
displayWelcome: function() {
console.log("=== Welcome Message ===");
const info = this.getInfo(); // 'this' refers to restaurant
console.log(`Welcome to ${info}!`);
},
// Method with nested function (common gotcha!)
processOrders: function() {
console.log("=== Processing Orders ===");
console.log("In processOrders, 'this' is:", this.name);
const orders = ["Pizza", "Pasta", "Salad"];
// PROBLEM: Regular function loses 'this' context
orders.forEach(function(order) {
console.log("Processing order:", order);
// console.log("Restaurant name:", this.name); // ERROR! 'this' is undefined here
});
console.log("Orders processed by:", this.name); // This works fine
},
// Solution: Using arrow function to preserve 'this'
processOrdersCorrect: function() {
console.log("=== Processing Orders (Correct Way) ===");
console.log("In processOrdersCorrect, 'this' is:", this.name);
const orders = ["Pizza", "Pasta", "Salad"];
// Arrow functions inherit 'this' from enclosing scope
orders.forEach((order) => {
console.log("Processing order:", order);
console.log("Restaurant name:", this.name); // This works! 'this' refers to restaurant
});
}
};
// Call methods - 'this' will refer to the restaurant object
restaurant.getInfo(); // 'this' = restaurant
restaurant.displayWelcome(); // 'this' = restaurant
restaurant.processOrders(); // 'this' = restaurant (but loses context in nested function)
restaurant.processOrdersCorrect(); // 'this' = restaurant (arrow function preserves context)
// ============= LOST CONTEXT - COMMON GOTCHA =============
// When you assign a method to a variable, it loses its object context
console.log("=== Lost Context Example ===");
const getRestaurantInfo = restaurant.getInfo; // Assign method to variable
// getRestaurantInfo(); // ERROR! 'this' is now undefined (or global object)
// Why does this happen? Because the function is no longer called as restaurant.getInfo()
// It's just called as getRestaurantInfo(), so there's no object before the dot
// ============= MULTIPLE OBJECTS SHARING METHODS =============
// The same function can work with different objects
const restaurant1 = {
name: "Tony's Deli",
location: "Chicago",
rating: 4.2,
getInfo: restaurant.getInfo // Share the same function
};
const restaurant2 = {
name: "Bella's Bistro",
location: "San Francisco",
rating: 4.8,
getInfo: restaurant.getInfo // Share the same function
};
console.log("=== Shared Methods ===");
restaurant1.getInfo(); // 'this' refers to restaurant1
restaurant2.getInfo(); // 'this' refers to restaurant2
// ============= CONSTRUCTOR FUNCTIONS AND 'THIS' =============
// When using 'new', 'this' refers to the newly created object
function Person(firstName, lastName, age) {
console.log("=== Constructor Function ===");
console.log("In constructor, 'this' is:", this);
// 'this' refers to the new object being created
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.introduce = function() {
console.log(`Hi, I'm ${this.firstName} ${this.lastName}`);
console.log(`I'm ${this.age} years old`);
console.log("In introduce method, 'this' is:", this);
};
this.celebrateBirthday = function() {
this.age++; // 'this' refers to the specific person object
console.log(`Happy birthday! ${this.firstName} is now ${this.age}`);
};
}
const person1 = new Person("Alice", "Johnson", 25);
const person2 = new Person("Bob", "Smith", 30);
person1.introduce(); // 'this' refers to person1
person2.introduce(); // 'this' refers to person2
person1.celebrateBirthday(); // 'this' refers to person1
// ============= ARROW FUNCTIONS AND 'THIS' =============
// Arrow functions don't have their own 'this' - they inherit it
const team = {
name: "Development Team",
members: ["Alice", "Bob", "Carol"],
// Regular method - has its own 'this'
showMembersWrong: function() {
console.log("=== Team Members (Wrong Way) ===");
console.log("Team name:", this.name); // 'this' refers to team
this.members.forEach(function(member) {
// Problem: regular function has its own 'this' (undefined)
console.log(`Member: ${member}`);
// console.log(`Team: ${this.name}`); // ERROR! 'this' is undefined
});
},
// Using arrow function - inherits 'this' from enclosing scope
showMembersRight: function() {
console.log("=== Team Members (Right Way) ===");
console.log("Team name:", this.name); // 'this' refers to team
this.members.forEach((member) => {
// Arrow function inherits 'this' from showMembersRight
console.log(`Member: ${member} works for ${this.name}`);
});
},
// Arrow function as method (usually not recommended)
arrowMethod: () => {
console.log("=== Arrow Function as Method ===");
console.log("In arrow method, 'this' is:", this);
// Arrow functions don't have their own 'this', so this refers to global scope
// console.log("Team name:", this.name); // ERROR! 'this' doesn't refer to team
}
};
team.showMembersWrong(); // Demonstrates the problem
team.showMembersRight(); // Shows the solution
team.arrowMethod(); // Shows why arrow functions aren't good for object methods
// ============= EVENT HANDLERS AND 'THIS' =============
// In event handlers, 'this' usually refers to the element that triggered the event
const button = {
text: "Click Me!",
clicks: 0,
// Method to handle clicks
handleClick: function() {
this.clicks++; // 'this' refers to the button object
console.log(`Button "${this.text}" clicked ${this.clicks} times`);
},
// Method to set up event listener (simulated)
setupEventListener: function() {
console.log("=== Event Listener Setup ===");
// Simulate adding event listener
const self = this; // Store reference to button object
// Simulated event handler
function simulatedClickEvent() {
console.log("Click event triggered!");
// In real DOM events, 'this' would refer to the DOM element
// So we use the stored reference 'self' to access the button object
self.handleClick();
}
// Simulate clicks
simulatedClickEvent();
simulatedClickEvent();
simulatedClickEvent();
}
};
button.setupEventListener();
// ============= METHOD BORROWING =============
// Using one object's method with another object's data
const calculator1 = {
number: 10,
double: function() {
console.log(`Doubling ${this.number}`);
this.number = this.number * 2;
console.log(`Result: ${this.number}`);
return this.number;
}
};
const calculator2 = {
number: 5
// No double method
};
console.log("=== Method Borrowing ===");
calculator1.double(); // Works normally
// Borrow the double method for calculator2
calculator1.double.call(calculator2); // 'this' inside double() refers to calculator2
console.log("Calculator2 number after borrowing:", calculator2.number);
// ============= DYNAMIC 'THIS' EXAMPLES =============
// Showing how 'this' changes based on call context
const mathOperations = {
value: 100,
add: function(amount) {
console.log(`Adding ${amount} to ${this.value}`);
this.value += amount;
return this.value;
},
subtract: function(amount) {
console.log(`Subtracting ${amount} from ${this.value}`);
this.value -= amount;
return this.value;
},
showValue: function() {
console.log(`Current value: ${this.value}`);
return this.value;
}
};
const anotherObject = {
value: 50
};
console.log("=== Dynamic 'this' Context ===");
// Normal method calls
mathOperations.showValue(); // 'this' = mathOperations, shows 100
mathOperations.add(25); // 'this' = mathOperations, value becomes 125
// Using methods with different objects
mathOperations.showValue.call(anotherObject); // 'this' = anotherObject, shows 50
mathOperations.add.call(anotherObject, 10); // 'this' = anotherObject, value becomes 60
console.log("Original object value:", mathOperations.value); // Still 125
console.log("Another object value:", anotherObject.value); // Now 60
// ============= PRACTICAL EXAMPLE - CHAINABLE CALCULATOR =============
// Building a calculator where methods can be chained
function ChainableCalculator(initialValue = 0) {
this.value = initialValue;
this.add = function(amount) {
console.log(`${this.value} + ${amount}`);
this.value += amount;
return this; // Return 'this' to enable chaining
};
this.subtract = function(amount) {
console.log(`${this.value} - ${amount}`);
this.value -= amount;
return this; // Return 'this' to enable chaining
};
this.multiply = function(amount) {
console.log(`${this.value} × ${amount}`);
this.value *= amount;
return this; // Return 'this' to enable chaining
};
this.getResult = function() {
console.log(`Final result: ${this.value}`);
return this.value;
};
}
console.log("=== Chainable Calculator ===");
const calc = new ChainableCalculator(10);
// Chain multiple operations - 'this' refers to the same calculator object throughout
const result = calc
.add(5) // 10 + 5 = 15
.multiply(2) // 15 × 2 = 30
.subtract(5) // 30 - 5 = 25
.getResult(); // Returns 25
console.log("Chained calculation result:", result);this refers to global object (window/global)this refers to the object before the dotthis refers to the newly created objectthis is inherited from enclosing scopethis usually refers to the element that triggered the eventWhile implicit binding lets JavaScript decide what this should be, explicit binding lets YOU decide what this should be. Think of it like being able to say "Hey function, I want you to work for THIS specific object right now."
// ============= THE PROBLEM WE'RE SOLVING =============
// Sometimes we want to use a method from one object with data from another object
const person1 = {
name: "Alice",
age: 25,
introduce: function() {
console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`);
return `${this.name}, age ${this.age}`;
}
};
const person2 = {
name: "Bob",
age: 30
// Bob doesn't have an introduce method
};
console.log("=== The Problem ===");
person1.introduce(); // Works fine - 'this' refers to person1
// person2.introduce(); // ERROR! person2 doesn't have this method
// How can we use person1's introduce method for person2's data?
// ============= CALL() - IMMEDIATE EXECUTION WITH EXPLICIT 'THIS' =============
// call() lets you call a function with a specific 'this' value
console.log("=== Using call() ===");
// Syntax: functionName.call(thisValue, arg1, arg2, arg3, ...)
person1.introduce.call(person2); // Use person1's method with person2's data
// call() with additional arguments
const calculator = {
value: 0,
add: function(a, b, c) {
console.log(`Starting with: ${this.value}`);
console.log(`Adding: ${a} + ${b} + ${c}`);
const sum = a + b + c;
this.value += sum;
console.log(`New value: ${this.value}`);
return this.value;
}
};
const storage1 = { value: 100 };
const storage2 = { value: 200 };
// Use calculator's add method with different storage objects
calculator.add.call(storage1, 10, 20, 30); // storage1.value becomes 160
calculator.add.call(storage2, 5, 15, 25); // storage2.value becomes 245
console.log("Storage1 value:", storage1.value); // 160
console.log("Storage2 value:", storage2.value); // 245
console.log("Original calculator value:", calculator.value); // Still 0
// ============= APPLY() - LIKE CALL() BUT WITH ARRAY OF ARGUMENTS =============
// apply() is identical to call(), but takes arguments as an array
console.log("=== Using apply() ===");
const numbers = [10, 20, 30];
// These two calls are identical:
calculator.add.call(storage1, 1, 2, 3); // Individual arguments
calculator.add.apply(storage1, [4, 5, 6]); // Array of arguments
// apply() is useful when you have arguments in an array
const mathOperations = {
name: "Math Helper",
findMax: function(numbers) {
console.log(`${this.name} finding max of:`, numbers);
const max = Math.max.apply(null, numbers); // Use apply to spread array
console.log(`Maximum value: ${max}`);
return max;
}
};
const dataSet1 = { name: "Dataset One" };
const dataSet2 = { name: "Dataset Two" };
const scores1 = [85, 92, 78, 96, 88];
const scores2 = [76, 84, 91, 87, 93];
// Use the same method with different datasets
mathOperations.findMax.apply(dataSet1, [scores1]);
mathOperations.findMax.apply(dataSet2, [scores2]);
// ============= BIND() - CREATE A NEW FUNCTION WITH BOUND 'THIS' =============
// bind() doesn't call the function immediately - it returns a new function with 'this' bound
console.log("=== Using bind() ===");
const greetingTemplate = {
message: "Welcome",
greet: function(name, timeOfDay) {
console.log(`${this.message}, ${name}! Have a great ${timeOfDay}!`);
return `${this.message} ${name}`;
}
};
const morningGreeting = { message: "Good morning" };
const eveningGreeting = { message: "Good evening" };
// Create new functions with bound 'this'
const sayGoodMorning = greetingTemplate.greet.bind(morningGreeting);
const sayGoodEvening = greetingTemplate.greet.bind(eveningGreeting);
// These are now independent functions with their 'this' permanently set
sayGoodMorning("Alice", "morning");
sayGoodEvening("Bob", "evening");
// You can also partially apply arguments with bind()
const sayGoodMorningToAlice = greetingTemplate.greet.bind(morningGreeting, "Alice");
sayGoodMorningToAlice("day"); // Only need to provide timeOfDay
sayGoodMorningToAlice("morning"); // Alice gets a good morning greeting
// ============= PRACTICAL EXAMPLE - EVENT HANDLER BINDING =============
// Common use case: maintaining object context in event handlers
function Button(label, clickCount = 0) {
this.label = label;
this.clickCount = clickCount;
this.handleClick = function() {
this.clickCount++;
console.log(`Button "${this.label}" clicked ${this.clickCount} times`);
};
// Method to set up event listener (simulated)
this.setupListener = function() {
console.log(`Setting up listener for button: ${this.label}`);
// Problem: In real event handlers, 'this' would refer to the DOM element
// Solution: Use bind() to ensure 'this' refers to our Button object
const boundHandler = this.handleClick.bind(this);
// Simulate event listener setup
console.log("Event listener bound successfully");
// Simulate clicks
boundHandler(); // Simulated click 1
boundHandler(); // Simulated click 2
boundHandler(); // Simulated click 3
};
}
console.log("=== Event Handler Binding ===");
const myButton = new Button("Save File");
myButton.setupListener();
// ============= FUNCTION BORROWING WITH EXPLICIT BINDING =============
// Using methods from one object type with another object type
const arrayLikeMethods = {
// Method that works with array-like objects
logItems: function() {
console.log(`Processing ${this.length} items:`);
// Use a for loop since 'this' might not be a real array
for (let i = 0; i < this.length; i++) {
console.log(` Item ${i}: ${this[i]}`);
}
},
// Method to find an item
findItem: function(searchValue) {
console.log(`Searching for: ${searchValue}`);
for (let i = 0; i < this.length; i++) {
if (this[i] === searchValue) {
console.log(`Found "${searchValue}" at index ${i}`);
return i;
}
}
console.log(`"${searchValue}" not found`);
return -1;
}
};
// Create array-like objects (have length and numbered properties)
const nodeList = {
0: "div",
1: "span",
2: "p",
length: 3
};
const argumentsObject = {
0: "first",
1: "second",
2: "third",
3: "fourth",
length: 4
};
console.log("=== Function Borrowing ===");
// Use array methods with non-array objects
arrayLikeMethods.logItems.call(nodeList);
arrayLikeMethods.logItems.call(argumentsObject);
arrayLikeMethods.findItem.call(nodeList, "span");
arrayLikeMethods.findItem.call(argumentsObject, "third");
// ============= CHAINING WITH EXPLICIT BINDING =============
// Creating a chainable calculator that can work with different data objects
function ChainableOperations() {
this.add = function(value) {
console.log(`Adding ${value} to ${this.current}`);
this.current = (this.current || 0) + value;
return this; // Return 'this' for chaining
};
this.multiply = function(value) {
console.log(`Multiplying ${this.current} by ${value}`);
this.current = (this.current || 0) * value;
return this; // Return 'this' for chaining
};
this.subtract = function(value) {
console.log(`Subtracting ${value} from ${this.current}`);
this.current = (this.current || 0) - value;
return this; // Return 'this' for chaining
};
this.getResult = function() {
console.log(`Final result: ${this.current}`);
return this.current;
};
}
const operations = new ChainableOperations();
// Different data objects to work with
const dataA = { current: 10 };
const dataB = { current: 50 };
console.log("=== Chainable Operations with Binding ===");
// Create bound versions that work with specific data objects
const operationsForA = {
add: operations.add.bind(dataA),
multiply: operations.multiply.bind(dataA),
subtract: operations.subtract.bind(dataA),
getResult: operations.getResult.bind(dataA)
};
const operationsForB = {
add: operations.add.bind(dataB),
multiply: operations.multiply.bind(dataB),
subtract: operations.subtract.bind(dataB),
getResult: operations.getResult.bind(dataB)
};
// Use chaining with different data objects
console.log("Operations with dataA:");
operationsForA.add(5).multiply(2).subtract(3).getResult(); // (10+5)*2-3 = 27
console.log("Operations with dataB:");
operationsForB.multiply(2).add(10).subtract(20).getResult(); // 50*2+10-20 = 90
console.log("Original data objects:");
console.log("dataA.current:", dataA.current); // 27
console.log("dataB.current:", dataB.current); // 90
// ============= REAL-WORLD EXAMPLE - VALIDATION SYSTEM =============
// Creating a flexible validation system using explicit binding
const validators = {
required: function(fieldName) {
if (!this.value || this.value.toString().trim() === '') {
this.errors.push(`${fieldName} is required`);
console.log(`❌ ${fieldName} validation failed: required`);
return false;
}
console.log(`✅ ${fieldName} validation passed: required`);
return true;
},
minLength: function(fieldName, minLen) {
if (!this.value || this.value.toString().length < minLen) {
this.errors.push(`${fieldName} must be at least ${minLen} characters`);
console.log(`❌ ${fieldName} validation failed: minimum length ${minLen}`);
return false;
}
console.log(`✅ ${fieldName} validation passed: minimum length ${minLen}`);
return true;
},
isEmail: function(fieldName) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!this.value || !emailRegex.test(this.value)) {
this.errors.push(`${fieldName} must be a valid email`);
console.log(`❌ ${fieldName} validation failed: invalid email format`);
return false;
}
console.log(`✅ ${fieldName} validation passed: valid email format`);
return true;
}
};
function validateField(fieldData, validationRules) {
console.log(`\n=== Validating ${fieldData.name} ===`);
console.log(`Value: "${fieldData.value}"`);
// Create validation context
const context = {
value: fieldData.value,
errors: []
};
// Apply each validation rule
validationRules.forEach(rule => {
if (rule.type === 'required') {
validators.required.call(context, fieldData.name);
} else if (rule.type === 'minLength') {
validators.minLength.call(context, fieldData.name, rule.value);
} else if (rule.type === 'isEmail') {
validators.isEmail.call(context, fieldData.name);
}
});
return {
isValid: context.errors.length === 0,
errors: context.errors
};
}
// Test the validation system
console.log("=== Validation System Example ===");
const emailField = { name: "Email", value: "user@example.com" };
const passwordField = { name: "Password", value: "123" };
const emptyField = { name: "Username", value: "" };
const emailRules = [
{ type: 'required' },
{ type: 'isEmail' }
];
const passwordRules = [
{ type: 'required' },
{ type: 'minLength', value: 8 }
];
const usernameRules = [
{ type: 'required' },
{ type: 'minLength', value: 3 }
];
const emailValidation = validateField(emailField, emailRules);
const passwordValidation = validateField(passwordField, passwordRules);
const usernameValidation = validateField(emptyField, usernameRules);
console.log("\n=== Validation Results ===");
console.log("Email valid:", emailValidation.isValid);
console.log("Password valid:", passwordValidation.isValid);
console.log("Username valid:", usernameValidation.isValid);| Method | Execution | Arguments | Use Case |
|---|---|---|---|
call() | Immediate | Individual parameters | When you know the arguments |
apply() | Immediate | Array of parameters | When arguments are in an array |
bind() | Returns new function | Can partially apply | Event handlers, creating reusable functions |
Promises are like ordering food at a restaurant. When you place an order, you get a receipt (the promise). You don't get your food immediately, but the receipt guarantees that you'll either get your food eventually (resolved) or find out there's a problem (rejected). While you wait, you can do other things.
// ============= UNDERSTANDING ASYNCHRONOUS PROBLEMS =============
// Why we need promises - the callback hell problem
console.log("=== The Problem: Callback Hell ===");
// Simulating asynchronous operations with setTimeout
function oldSchoolAsyncOperation(data, callback) {
console.log(`Starting operation with: ${data}`);
setTimeout(() => {
if (data) {
console.log(`Operation completed successfully`);
callback(null, `Processed: ${data}`);
} else {
console.log(`Operation failed`);
callback(new Error("No data provided"), null);
}
}, 1000);
}
// This creates "callback hell" - nested callbacks are hard to read and manage
function demonstrateCallbackHell() {
oldSchoolAsyncOperation("Step 1", (error1, result1) => {
if (error1) {
console.log("Error in step 1:", error1.message);
return;
}
oldSchoolAsyncOperation(result1, (error2, result2) => {
if (error2) {
console.log("Error in step 2:", error2.message);
return;
}
oldSchoolAsyncOperation(result2, (error3, result3) => {
if (error3) {
console.log("Error in step 3:", error3.message);
return;
}
console.log("Final result:", result3);
// Imagine this going even deeper...
});
});
});
}
// demonstrateCallbackHell(); // Uncomment to see callback hell in action
// ============= BASIC PROMISE CREATION AND USAGE =============
// Promises provide a cleaner way to handle asynchronous operations
console.log("=== Basic Promise Creation ===");
// Creating a promise - it's like making a commitment to do something
const simplePromise = new Promise((resolve, reject) => {
// The promise executor function runs immediately
console.log("Promise executor is running...");
// Simulate some asynchronous work
setTimeout(() => {
const success = Math.random() > 0.3; // 70% chance of success
if (success) {
console.log("Promise operation succeeded!");
resolve("Success! Here's your data."); // Fulfill the promise
} else {
console.log("Promise operation failed!");
reject(new Error("Something went wrong!")); // Reject the promise
}
}, 1500);
});
// Using the promise with .then() and .catch()
simplePromise
.then((result) => {
// This runs if the promise resolves (succeeds)
console.log("✅ Promise resolved with:", result);
})
.catch((error) => {
// This runs if the promise rejects (fails)
console.log("❌ Promise rejected with:", error.message);
})
.finally(() => {
// This always runs, regardless of success or failure
console.log("🏁 Promise completed (either resolved or rejected)");
});
// ============= PROMISE STATES AND LIFECYCLE =============
// Understanding the three states of a promise
function demonstratePromiseStates() {
console.log("=== Promise States Demo ===");
// State 1: Pending (initial state)
const pendingPromise = new Promise((resolve, reject) => {
console.log("Promise is pending...");
// Don't call resolve or reject - keeps it pending
});
console.log("Pending promise state:", pendingPromise);
// State 2: Fulfilled (resolved)
const fulfilledPromise = Promise.resolve("I'm fulfilled!");
console.log("Fulfilled promise:", fulfilledPromise);
// State 3: Rejected
const rejectedPromise = Promise.reject(new Error("I'm rejected!"));
console.log("Rejected promise:", rejectedPromise);
// Handle the rejected promise to prevent unhandled rejection warning
rejectedPromise.catch(error => console.log("Caught:", error.message));
}
demonstratePromiseStates();
// ============= PROMISE CHAINING - SOLVING CALLBACK HELL =============
// Promises can be chained to avoid nested callbacks
console.log("=== Promise Chaining ===");
function createAsyncStep(stepName, delay = 1000) {
return new Promise((resolve, reject) => {
console.log(`Starting ${stepName}...`);
setTimeout(() => {
const success = Math.random() > 0.2; // 80% success rate
if (success) {
const result = `${stepName} completed successfully`;
console.log(`✅ ${result}`);
resolve(result);
} else {
const error = new Error(`${stepName} failed`);
console.log(`❌ ${error.message}`);
reject(error);
}
}, delay);
});
}
// Chain promises instead of nesting callbacks
createAsyncStep("Database connection", 500)
.then((result1) => {
console.log("Step 1 result:", result1);
// Return another promise to continue the chain
return createAsyncStep("User authentication", 800);
})
.then((result2) => {
console.log("Step 2 result:", result2);
// Return another promise
return createAsyncStep("Data processing", 600);
})
.then((result3) => {
console.log("Step 3 result:", result3);
// Return a regular value (automatically wrapped in a resolved promise)
return "All operations completed successfully!";
})
.then((finalResult) => {
console.log("🎉 Final result:", finalResult);
})
.catch((error) => {
// This catches any error from any step in the chain
console.log("💥 Chain failed at some step:", error.message);
})
.finally(() => {
console.log("🏁 Promise chain completed");
});
// ============= REAL-WORLD EXAMPLE - FETCHING USER DATA =============
// Simulating a real application that fetches user data
class UserService {
// Simulate fetching user basic info
static fetchUserInfo(userId) {
return new Promise((resolve, reject) => {
console.log(`Fetching user info for ID: ${userId}`);
setTimeout(() => {
if (userId > 0) {
const userInfo = {
id: userId,
name: `User ${userId}`,
email: `user${userId}@example.com`
};
console.log("User info fetched:", userInfo);
resolve(userInfo);
} else {
reject(new Error("Invalid user ID"));
}
}, 800);
});
}
// Simulate fetching user preferences
static fetchUserPreferences(userId) {
return new Promise((resolve, reject) => {
console.log(`Fetching preferences for user ${userId}`);
setTimeout(() => {
const preferences = {
theme: "dark",
language: "en",
notifications: true
};
console.log("User preferences fetched:", preferences);
resolve(preferences);
}, 600);
});
}
// Simulate fetching user posts
static fetchUserPosts(userId) {
return new Promise((resolve, reject) => {
console.log(`Fetching posts for user ${userId}`);
setTimeout(() => {
const posts = [
{ id: 1, title: "My first post", likes: 10 },
{ id: 2, title: "Another great post", likes: 25 },
{ id: 3, title: "Latest thoughts", likes: 5 }
];
console.log("User posts fetched:", posts.length, "posts");
resolve(posts);
}, 1000);
});
}
}
// Using the UserService with promise chaining
function loadUserProfile(userId) {
console.log("=== Loading User Profile ===");
let userData = {}; // Store accumulated data
return UserService.fetchUserInfo(userId)
.then((userInfo) => {
userData.info = userInfo;
console.log("User info loaded, now fetching preferences...");
// Return another promise to continue the chain
return UserService.fetchUserPreferences(userId);
})
.then((preferences) => {
userData.preferences = preferences;
console.log("Preferences loaded, now fetching posts...");
return UserService.fetchUserPosts(userId);
})
.then((posts) => {
userData.posts = posts;
console.log("All user data loaded successfully!");
// Return the complete user profile
return userData;
})
.catch((error) => {
console.log("Failed to load user profile:", error.message);
throw error; // Re-throw to let caller handle it
});
}
// Use the user profile loader
loadUserProfile(123)
.then((profile) => {
console.log("Complete user profile:", profile);
})
.catch((error) => {
console.log("Error loading profile:", error.message);
});
// ============= PROMISE UTILITY METHODS =============
// JavaScript provides several utility methods for working with multiple promises
console.log("=== Promise Utility Methods ===");
// Promise.all() - Wait for ALL promises to complete (fail fast)
console.log("Testing Promise.all()...");
const promise1 = createAsyncStep("Task A", 500);
const promise2 = createAsyncStep("Task B", 800);
const promise3 = createAsyncStep("Task C", 300);
Promise.all([promise1, promise2, promise3])
.then((results) => {
console.log("✅ Promise.all() - All tasks completed:");
results.forEach((result, index) => {
console.log(` Task ${index + 1}: ${result}`);
});
})
.catch((error) => {
console.log("❌ Promise.all() - One or more tasks failed:", error.message);
});
// Promise.allSettled() - Wait for ALL promises to settle (doesn't fail fast)
console.log("Testing Promise.allSettled()...");
const mixedPromises = [
Promise.resolve("Success 1"),
Promise.reject(new Error("Failure 1")),
Promise.resolve("Success 2"),
createAsyncStep("Task D", 400)
];
Promise.allSettled(mixedPromises)
.then((results) => {
console.log("Promise.allSettled() - All promises settled:");
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(` ✅ Promise ${index + 1}: ${result.value}`);
} else {
console.log(` ❌ Promise ${index + 1}: ${result.reason.message}`);
}
});
});
// Promise.race() - Return the