Content is user-generated and unverified.

Complete JavaScript Interview Guide - 360° Detailed Explanations

1. var, let, and const - Variable Declarations

The Big Picture

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.

Detailed Code Examples

javascript
// ============= 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!)
    }
}

Interview Questions & Gotchas

  • Q: Why does var in a loop cause issues? A: Because var doesn't respect block scope, all iterations share the same variable.
  • Q: Can you change a const object? A: You can modify its properties, but not reassign the entire object.

2. map, filter, and reduce - Array Transformation Methods

The Big Picture

Think of these as different machines in a factory assembly line. Each machine does a specific job with the items that come through.

Detailed Code Examples

javascript
// ============= 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 }

Common Mistakes & Interview Questions

  • Q: What's the difference between map and forEach? A: map returns a new array, forEach just executes code for each element.
  • Q: When should you use reduce vs map/filter? A: Use reduce when you need to transform an array into a single value or different data structure.

3. Functions - Different Ways to Create and Use Functions

The Big Picture

Functions are like recipes. You can write recipes in different ways, but they all tell you how to make something.

Detailed Code Examples

javascript
// ============= 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");

Interview Questions & Gotchas

  • Q: What's the difference between function declaration and expression? A: Declarations are hoisted, expressions are not.
  • Q: When should you use arrow functions? A: When you don't need their own 'this' context, especially for short functions.
  • Q: What happens if you don't return anything from a function? A: It returns 'undefined' automatically.

4. Closures - Functions That Remember Their Environment

The Big Picture

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.

Detailed Code Examples

javascript
// ============= 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); // undefined

Real-World Applications & Interview Questions

  • Q: What is a closure? A: A function that has access to variables from its outer (enclosing) scope even after the outer function has finished executing.
  • Q: Why do closures matter? A: They enable data privacy, function factories, and the module pattern.
  • Q: What's the difference between closure and scope? A: Scope determines where variables can be accessed; closure is when a function "remembers" its scope even when executed elsewhere.

5. Currying - Breaking Down Functions into Single-Argument Steps

The Big Picture

Currying 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.

Detailed Code Examples

javascript
// ============= 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" }));

Interview Questions & Benefits

  • Q: What is currying? A: Transforming a function that takes multiple arguments into a sequence of functions that each take a single argument.
  • Q: Why use currying? A: It enables partial application, creates reusable specialized functions, and makes code more modular and testable.
  • Q: Difference between currying and partial application? A: Currying always returns unary functions (one argument), while partial application can fix any number of arguments.

6. Objects - Creating, Manipulating, and Understanding JavaScript Objects

The Big Picture

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.

Detailed Code Examples

javascript
// ============= 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);      // false

Interview Questions & Important Concepts

  • Q: What's the difference between dot notation and bracket notation? A: Dot notation is cleaner but requires valid identifiers; bracket notation allows dynamic property access and special characters.
  • Q: How do you check if a property exists in an object? A: Use hasOwnProperty(), in operator, or check !== undefined.
  • Q: What happens when you try to access a non-existent property? A: JavaScript returns undefined rather than throwing an error.
  • Q: What's the difference between Object.assign() and the spread operator? A: Both create shallow copies, but spread operator is more modern and readable.

7. 'this' in JavaScript - Understanding Context and Implicit Binding

The Big Picture

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.

Detailed Code Examples

javascript
// ============= 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);

Key Rules for 'this' (Summary)

  1. Global Context: this refers to global object (window/global)
  2. Object Method: this refers to the object before the dot
  3. Constructor Function: this refers to the newly created object
  4. Arrow Functions: this is inherited from enclosing scope
  5. Event Handlers: this usually refers to the element that triggered the event
  6. Lost Context: Assigning methods to variables loses the object context

Interview Questions & Gotchas

  • Q: What does 'this' refer to in a regular function vs arrow function? A: Regular functions have their own 'this' context, arrow functions inherit 'this' from their lexical scope.
  • Q: Why might 'this' be undefined in a method? A: If the method is assigned to a variable and called without an object context, or in strict mode.
  • Q: How can you preserve 'this' context in nested functions? A: Use arrow functions, store 'this' in a variable (like 'self'), or use bind/call/apply.

8. call, bind, and apply - Explicit Binding of 'this'

The Big Picture

While 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."

Detailed Code Examples

javascript
// ============= 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);

Key Differences Summary

MethodExecutionArgumentsUse Case
call()ImmediateIndividual parametersWhen you know the arguments
apply()ImmediateArray of parametersWhen arguments are in an array
bind()Returns new functionCan partially applyEvent handlers, creating reusable functions

Interview Questions & Best Practices

  • Q: When would you use bind() vs call()? A: Use bind() when you need to create a new function for later use (like event handlers). Use call() when you want immediate execution.
  • Q: What's the performance difference? A: call() and apply() are slightly faster than bind() since bind() creates a new function.
  • Q: Can you use these methods on arrow functions? A: You can call them, but they won't change the 'this' context since arrow functions don't have their own 'this'.

9. Promises and Async JavaScript - Handling Asynchronous Operations

The Big Picture

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.

Detailed Code Examples

javascript
// ============= 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
Content is user-generated and unverified.
    Complete JavaScript Interview Guide - 360° Detailed Explanations | Claude