JavaScript Advanced (Part 1)

Master advanced JavaScript concepts including closures, prototypes, ES6+ features, and functional programming.

JavaScript Advanced Content

This content has been divided into two parts for better readability:

Closures and Scope

Closures are one of the most powerful features in JavaScript. Understanding closures and scope is essential for writing advanced JavaScript code.

Execution Context and Scope Chain

Every time a function is called, a new execution context is created. The execution context includes the scope chain, which determines variable access.

// Global scope
const globalVar = 'I am global';

function outerFunction() {
    // outerFunction scope
    const outerVar = 'I am from outer';
    
    function innerFunction() {
        // innerFunction scope
        const innerVar = 'I am from inner';
        
        console.log(innerVar); // Accessible: own scope
        console.log(outerVar); // Accessible: parent function's scope
        console.log(globalVar); // Accessible: global scope
    }
    
    innerFunction();
    
    // console.log(innerVar); // Error: not accessible
}

outerFunction();

Understanding Closures

A closure is a function that has access to its own scope, the scope of the outer function, and the global scope.

function createCounter() {
    let count = 0; // Private variable
    
    return {
        increment: function() {
            count++;
            return count;
        },
        decrement: function() {
            count--;
            return count;
        },
        getCount: function() {
            return count;
        }
    };
}

const counter = createCounter();
console.log(counter.getCount()); // 0
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1

// count is not directly accessible
// console.log(counter.count); // undefined

Practical Applications of Closures

Data Privacy and Encapsulation

function createBankAccount(initialBalance) {
    let balance = initialBalance;
    
    // Private function
    function validateAmount(amount) {
        return typeof amount === 'number' && amount > 0;
    }
    
    return {
        deposit: function(amount) {
            if (!validateAmount(amount)) {
                return 'Invalid amount';
            }
            balance += amount;
            return `Deposited ${amount}. New balance: ${balance}`;
        },
        withdraw: function(amount) {
            if (!validateAmount(amount)) {
                return 'Invalid amount';
            }
            if (amount > balance) {
                return 'Insufficient funds';
            }
            balance -= amount;
            return `Withdrew ${amount}. New balance: ${balance}`;
        },
        getBalance: function() {
            return `Current balance: ${balance}`;
        }
    };
}

const account = createBankAccount(100);
console.log(account.getBalance()); // Current balance: 100
console.log(account.deposit(50)); // Deposited 50. New balance: 150
console.log(account.withdraw(30)); // Withdrew 30. New balance: 120
console.log(account.withdraw(200)); // Insufficient funds

Function Factories

function createMultiplier(multiplier) {
    return function(number) {
        return number * multiplier;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);
const quadruple = createMultiplier(4);

console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(quadruple(5)); // 20

Memoization

function memoize(fn) {
    const cache = {};
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (cache[key]) {
            console.log('Fetching from cache');
            return cache[key];
        }
        
        console.log('Calculating result');
        const result = fn.apply(this, args);
        cache[key] = result;
        
        return result;
    };
}

// Expensive function to calculate fibonacci numbers
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// Memoized version
const memoizedFibonacci = memoize(function(n) {
    if (n <= 1) return n;
    return memoizedFibonacci(n - 1) + memoizedFibonacci(n - 2);
});

console.time('Non-memoized');
console.log(fibonacci(35)); // Very slow
console.timeEnd('Non-memoized');

console.time('Memoized');
console.log(memoizedFibonacci(35)); // Much faster
console.timeEnd('Memoized');

Common Closure Pitfalls

Loop Closures

// Problem: All closures share the same reference to i
function createFunctions() {
    const functions = [];
    
    for (var i = 0; i < 5; i++) {
        functions.push(function() {
            return i;
        });
    }
    
    return functions;
}

const functions = createFunctions();
for (let i = 0; i < functions.length; i++) {
    console.log(functions[i]()); // All output 5
}

// Solution 1: Use let instead of var (block scope)
function createFunctionsFixed1() {
    const functions = [];
    
    for (let i = 0; i < 5; i++) {
        functions.push(function() {
            return i;
        });
    }
    
    return functions;
}

const functionsFixed1 = createFunctionsFixed1();
for (let i = 0; i < functionsFixed1.length; i++) {
    console.log(functionsFixed1[i]()); // 0, 1, 2, 3, 4
}

// Solution 2: Use an IIFE to create a new scope
function createFunctionsFixed2() {
    const functions = [];
    
    for (var i = 0; i < 5; i++) {
        functions.push((function(value) {
            return function() {
                return value;
            };
        })(i));
    }
    
    return functions;
}

const functionsFixed2 = createFunctionsFixed2();
for (let i = 0; i < functionsFixed2.length; i++) {
    console.log(functionsFixed2[i]()); // 0, 1, 2, 3, 4
}

Memory Leaks

// Potential memory leak
function setupEventListener() {
    const element = document.getElementById('button');
    const largeData = new Array(10000000).fill('data');
    
    element.addEventListener('click', function() {
        // This closure holds a reference to largeData
        console.log('Button clicked', largeData.length);
    });
}

// Better approach
function setupEventListenerFixed() {
    const element = document.getElementById('button');
    
    element.addEventListener('click', function() {
        // No reference to large data
        console.log('Button clicked');
    });
    
    // Process large data here and let it be garbage collected
    const largeData = new Array(10000000).fill('data');
    processData(largeData);
}

Prototypes and Inheritance

JavaScript uses a prototype-based inheritance model, which is different from the class-based inheritance model used in many other languages.

Understanding Prototypes

Every JavaScript object has a prototype, which is another object that it inherits properties and methods from.

// Creating an object
const person = {
    name: 'John',
    greet: function() {
        return `Hello, my name is ${this.name}`;
    }
};

// person's prototype is Object.prototype
console.log(Object.getPrototypeOf(person) === Object.prototype); // true

// Creating an object with a specific prototype
const employee = Object.create(person);
employee.jobTitle = 'Developer';
employee.introduce = function() {
    return `${this.greet()}. I work as a ${this.jobTitle}`;
};

// employee's prototype is person
console.log(Object.getPrototypeOf(employee) === person); // true

// Accessing inherited properties
console.log(employee.name); // John (inherited from person)
console.log(employee.greet()); // Hello, my name is John (inherited from person)
console.log(employee.introduce()); // Hello, my name is John. I work as a Developer

// Modifying the prototype
person.name = 'Jane';
console.log(employee.name); // Jane (the change is reflected in the prototype chain)

Constructor Functions and the 'new' Keyword

Constructor functions are used to create objects with a specific prototype.

// Constructor function
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// Adding methods to the prototype
Person.prototype.greet = function() {
    return `Hello, my name is ${this.name} and I am ${this.age} years old`;
};

// Creating objects with the constructor
const john = new Person('John', 30);
const jane = new Person('Jane', 25);

console.log(john.greet()); // Hello, my name is John and I am 30 years old
console.log(jane.greet()); // Hello, my name is Jane and I am 25 years old

// What happens when we use 'new'
// 1. A new empty object is created
// 2. The constructor function is called with 'this' set to the new object
// 3. The new object's prototype is set to the constructor's prototype
// 4. The new object is returned (unless the constructor returns something else)

// Checking the prototype chain
console.log(john.__proto__ === Person.prototype); // true
console.log(john.__proto__.__proto__ === Object.prototype); // true

Prototypal Inheritance

Prototypal inheritance allows objects to inherit properties and methods from other objects.

// Parent constructor
function Animal(name) {
    this.name = name;
}

Animal.prototype.eat = function() {
    return `${this.name} is eating`;
};

// Child constructor
function Dog(name, breed) {
    // Call the parent constructor
    Animal.call(this, name);
    this.breed = breed;
}

// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Fix the constructor property

// Add methods to the child prototype
Dog.prototype.bark = function() {
    return `${this.name} is barking`;
};

// Create an instance
const rex = new Dog('Rex', 'German Shepherd');

console.log(rex.name); // Rex
console.log(rex.breed); // German Shepherd
console.log(rex.eat()); // Rex is eating (inherited from Animal)
console.log(rex.bark()); // Rex is barking

// Check the prototype chain
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true

ES6 Classes (Syntactic Sugar)

ES6 introduced class syntax, which is syntactic sugar over JavaScript's prototype-based inheritance.

// ES6 class syntax
class Animal {
    constructor(name) {
        this.name = name;
    }
    
    eat() {
        return `${this.name} is eating`;
    }
    
    static isAnimal(obj) {
        return obj instanceof Animal;
    }
}

// Inheritance with class syntax
class Dog extends Animal {
    constructor(name, breed) {
        super(name); // Call the parent constructor
        this.breed = breed;
    }
    
    bark() {
        return `${this.name} is barking`;
    }
    
    // Override parent method
    eat() {
        return `${super.eat()} dog food`;
    }
}

const rex = new Dog('Rex', 'German Shepherd');

console.log(rex.name); // Rex
console.log(rex.breed); // German Shepherd
console.log(rex.eat()); // Rex is eating dog food
console.log(rex.bark()); // Rex is barking

// Static methods
console.log(Animal.isAnimal(rex)); // true

// Under the hood, this is still using prototypes
console.log(rex.__proto__ === Dog.prototype); // true
console.log(rex.__proto__.__proto__ === Animal.prototype); // true

Prototype Methods and Properties

JavaScript provides several methods for working with prototypes.

// Object.create()
const personProto = {
    greet() {
        return `Hello, my name is ${this.name}`;
    }
};

const john = Object.create(personProto);
john.name = 'John';
console.log(john.greet()); // Hello, my name is John

// Object.getPrototypeOf() and Object.setPrototypeOf()
console.log(Object.getPrototypeOf(john) === personProto); // true

const customProto = {
    sayHi() {
        return `Hi, I'm ${this.name}`;
    }
};

Object.setPrototypeOf(john, customProto);
console.log(john.sayHi()); // Hi, I'm John
// console.log(john.greet()); // Error: john.greet is not a function

// Object.hasOwnProperty()
console.log(john.hasOwnProperty('name')); // true
console.log(john.hasOwnProperty('sayHi')); // false (it's on the prototype)

// for...in loop (includes prototype properties)
for (const prop in john) {
    console.log(prop); // name, sayHi
}

// Object.keys() (only own properties)
console.log(Object.keys(john)); // ['name']

// Object.getOwnPropertyNames() (all own properties, including non-enumerable)
console.log(Object.getOwnPropertyNames(john)); // ['name']

Mixins and Composition

Mixins provide a way to add functionality to objects without inheritance.

// Mixin pattern
const swimmingMixin = {
    swim() {
        return `${this.name} is swimming`;
    }
};

const flyingMixin = {
    fly() {
        return `${this.name} is flying`;
    }
};

// Using mixins with constructor functions
function Duck(name) {
    this.name = name;
}

// Copy methods from mixins to the prototype
Object.assign(Duck.prototype, swimmingMixin, flyingMixin);

const donald = new Duck('Donald');
console.log(donald.swim()); // Donald is swimming
console.log(donald.fly()); // Donald is flying

// Using mixins with classes
class Fish {
    constructor(name) {
        this.name = name;
    }
}

Object.assign(Fish.prototype, swimmingMixin);

const nemo = new Fish('Nemo');
console.log(nemo.swim()); // Nemo is swimming

// Composition over inheritance
function createAnimal(name) {
    return {
        name,
        eat() {
            return `${name} is eating`;
        }
    };
}

function createSwimmer(animal) {
    return {
        ...animal,
        swim() {
            return `${animal.name} is swimming`;
        }
    };
}

function createFlyer(animal) {
    return {
        ...animal,
        fly() {
            return `${animal.name} is flying`;
        }
    };
}

// Create a duck using composition
const daffy = createFlyer(createSwimmer(createAnimal('Daffy')));
console.log(daffy.eat()); // Daffy is eating
console.log(daffy.swim()); // Daffy is swimming
console.log(daffy.fly()); // Daffy is flying

ES6+ Features

ECMAScript 6 (ES6) and later versions introduced many new features that have transformed how we write JavaScript.

Arrow Functions

Arrow functions provide a concise syntax for writing functions and have lexical this binding.

// Traditional function expression
const add = function(a, b) {
    return a + b;
};

// Arrow function
const addArrow = (a, b) => a + b;

console.log(add(2, 3)); // 5
console.log(addArrow(2, 3)); // 5

// Arrow functions with multiple statements
const calculate = (a, b) => {
    const sum = a + b;
    const product = a * b;
    return { sum, product };
};

console.log(calculate(2, 3)); // { sum: 5, product: 6 }

// Lexical 'this' binding
function Counter() {
    this.count = 0;
    
    // Traditional function loses 'this' context
    setInterval(function() {
        this.count++; // 'this' refers to the global object, not Counter
        console.log(this.count); // NaN
    }, 1000);
}

function CounterFixed() {
    this.count = 0;
    
    // Arrow function preserves 'this' context
    setInterval(() => {
        this.count++; // 'this' refers to CounterFixed instance
        console.log(this.count); // 1, 2, 3, ...
    }, 1000);
}

// new Counter(); // Incorrect behavior
// new CounterFixed(); // Correct behavior

Destructuring

Destructuring allows you to extract values from arrays and objects into distinct variables.

// Array destructuring
const numbers = [1, 2, 3, 4, 5];

// Basic destructuring
const [first, second, ...rest] = numbers;
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]

// Skipping elements
const [a, , c] = numbers;
console.log(a, c); // 1 3

// Default values
const [x = 10, y = 20, z = 30] = [1, 2];
console.log(x, y, z); // 1 2 30

// Swapping variables
let m = 1;
let n = 2;
[m, n] = [n, m];
console.log(m, n); // 2 1

// Object destructuring
const person = {
    name: 'John',
    age: 30,
    address: {
        city: 'New York',
        country: 'USA'
    }
};

// Basic destructuring
const { name, age } = person;
console.log(name, age); // John 30

// Renaming variables
const { name: fullName, age: years } = person;
console.log(fullName, years); // John 30

// Default values
const { name: userName = 'Anonymous', job = 'Unknown' } = person;
console.log(userName, job); // John Unknown

// Nested destructuring
const { address: { city, country } } = person;
console.log(city, country); // New York USA

// Rest operator with objects
const { name: personName, ...personDetails } = person;
console.log(personName); // John
console.log(personDetails); // { age: 30, address: { city: 'New York', country: 'USA' } }

// Function parameter destructuring
function printPerson({ name, age, address: { city } = {} } = {}) {
    console.log(`${name}, ${age}, ${city}`);
}

printPerson(person); // John, 30, New York
printPerson({ name: 'Jane', age: 25 }); // Jane, 25, undefined
printPerson(); // undefined, undefined, undefined

Spread and Rest Operators

The spread operator (...) expands an iterable into individual elements, while the rest operator collects multiple elements into a single array.

// Spread operator with arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// Combining arrays
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]

// Copying arrays
const copy = [...arr1];
copy.push(4);
console.log(arr1); // [1, 2, 3] (original unchanged)
console.log(copy); // [1, 2, 3, 4]

// Spread operator with objects
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };

// Combining objects
const combinedObj = { ...obj1, ...obj2 };
console.log(combinedObj); // { a: 1, b: 2, c: 3, d: 4 }

// Copying objects
const copyObj = { ...obj1 };
copyObj.a = 10;
console.log(obj1); // { a: 1, b: 2 } (original unchanged)
console.log(copyObj); // { a: 10, b: 2 }

// Overriding properties
const overrideObj = { ...obj1, a: 10 };
console.log(overrideObj); // { a: 10, b: 2 }

// Rest operator in function parameters
function sum(...numbers) {
    return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3, 4, 5)); // 15

// Combining rest and normal parameters
function multiply(multiplier, ...numbers) {
    return numbers.map(num => num * multiplier);
}

console.log(multiply(2, 1, 2, 3, 4)); // [2, 4, 6, 8]

Template Literals

Template literals provide an improved way to work with strings, supporting multi-line strings and string interpolation.

// Basic template literals
const name = 'John';
const greeting = `Hello, ${name}!`;
console.log(greeting); // Hello, John!

// Multi-line strings
const multiLine = `This is a
multi-line
string`;
console.log(multiLine);
// This is a
// multi-line
// string

// Expressions in template literals
const a = 5;
const b = 10;
console.log(`The sum of ${a} and ${b} is ${a + b}`); // The sum of 5 and 10 is 15

// Nested template literals
const nested = `The result is ${a > b ? `${a} is greater than ${b}` : `${a} is less than or equal to ${b}`}`;
console.log(nested); // The result is 5 is less than or equal to 10

// Tagged template literals
function highlight(strings, ...values) {
    return strings.reduce((result, str, i) => {
        const value = values[i] || '';
        return `${result}${str}${value}`;
    }, '');
}

const user = 'John';
const role = 'Admin';
const highlightedText = highlight`User ${user} has role ${role}`;
console.log(highlightedText); // User John has role Admin

Default Parameters and Rest/Spread

Default parameters allow you to specify default values for function parameters, while rest and spread operators provide flexible ways to work with arrays and objects.

// Default parameters
function greet(name = 'Anonymous', greeting = 'Hello') {
    return `${greeting}, ${name}!`;
}

console.log(greet()); // Hello, Anonymous!
console.log(greet('John')); // Hello, John!
console.log(greet('Jane', 'Hi')); // Hi, Jane!

// Expressions as default values
function calculateTax(amount, taxRate = amount * 0.1) {
    return amount + taxRate;
}

console.log(calculateTax(100)); // 110
console.log(calculateTax(100, 20)); // 120

// Default parameters with destructuring
function createUser({ name = 'Anonymous', age = 0, isAdmin = false } = {}) {
    return { name, age, isAdmin };
}

console.log(createUser()); // { name: 'Anonymous', age: 0, isAdmin: false }
console.log(createUser({ name: 'John', age: 30 })); // { name: 'John', age: 30, isAdmin: false }

// Rest parameters
function logArguments(first, ...rest) {
    console.log('First argument:', first);
    console.log('Rest of arguments:', rest);
}

logArguments(1, 2, 3, 4, 5);
// First argument: 1
// Rest of arguments: [2, 3, 4, 5]

// Spread operator with function calls
function sum(a, b, c) {
    return a + b + c;
}

const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6

Modules

ES6 modules provide a standardized way to organize and share code between JavaScript files.

// math.js
export const PI = 3.14159;

export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

export default function multiply(a, b) {
    return a * b;
}

// utils.js
export function formatDate(date) {
    return date.toLocaleDateString();
}

export function formatCurrency(amount) {
    return `$${amount.toFixed(2)}`;
}

// main.js
// Named imports
import { add, subtract, PI } from './math.js';
console.log(add(2, 3)); // 5
console.log(subtract(5, 2)); // 3
console.log(PI); // 3.14159

// Default import
import multiply from './math.js';
console.log(multiply(2, 3)); // 6

// Renaming imports
import { add as sum } from './math.js';
console.log(sum(2, 3)); // 5

// Importing all exports as an object
import * as mathUtils from './math.js';
console.log(mathUtils.add(2, 3)); // 5
console.log(mathUtils.PI); // 3.14159
console.log(mathUtils.default(2, 3)); // 6

// Combining imports
import multiply2, { add as addition, PI as piValue } from './math.js';

// Dynamic imports
async function loadModule() {
    const math = await import('./math.js');
    console.log(math.add(2, 3)); // 5
}

Promises and Async/Await

Promises and async/await provide improved ways to work with asynchronous code.

// Creating a promise
const fetchData = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const data = { id: 1, name: 'John' };
            // Simulate success
            resolve(data);
            // Simulate error
            // reject(new Error('Failed to fetch data'));
        }, 1000);
    });
};

// Using promises
fetchData()
    .then(data => {
        console.log('Data:', data);
        return data.id;
    })
    .then(id => {
        console.log('ID:', id);
        return fetchData(); // Return another promise
    })
    .then(data => {
        console.log('Data 2:', data);
    })
    .catch(error => {
        console.error('Error:', error.message);
    })
    .finally(() => {
        console.log('Promise completed');
    });

// Promise.all - wait for all promises to resolve
Promise.all([
    fetchData(),
    fetchData(),
    fetchData()
])
    .then(results => {
        console.log('All results:', results);
    })
    .catch(error => {
        console.error('Error in any promise:', error.message);
    });

// Promise.race - wait for the first promise to resolve or reject
Promise.race([
    fetchData(),
    new Promise((resolve) => setTimeout(() => resolve('Timeout'), 500))
])
    .then(result => {
        console.log('First result:', result);
    })
    .catch(error => {
        console.error('Error:', error.message);
    });

// Async/await
async function fetchDataAsync() {
    try {
        const data = await fetchData();
        console.log('Async data:', data);
        
        const id = data.id;
        console.log('Async ID:', id);
        
        const data2 = await fetchData();
        console.log('Async data 2:', data2);
        
        return 'Done';
    } catch (error) {
        console.error('Async error:', error.message);
        throw error; // Re-throw the error
    } finally {
        console.log('Async completed');
    }
}

// Call the async function
fetchDataAsync()
    .then(result => {
        console.log('Async result:', result);
    })
    .catch(error => {
        console.error('Caught async error:', error.message);
    });

Other ES6+ Features

ES6 and later versions introduced many other useful features.

// let and const
let x = 10;
x = 20; // Allowed
const y = 30;
// y = 40; // Error: Assignment to constant variable

// Block scope
{
    let blockScoped = 'visible only in this block';
    var functionScoped = 'visible in the entire function';
}
// console.log(blockScoped); // Error: blockScoped is not defined
console.log(functionScoped); // 'visible in the entire function'

// Map and Set
const map = new Map();
map.set('key1', 'value1');
map.set('key2', 'value2');
map.set(123, 'numeric key');
map.set({}, 'object key');

console.log(map.get('key1')); // value1
console.log(map.has('key2')); // true
console.log(map.size); // 4

const set = new Set([1, 2, 3, 3, 4, 4, 5]);
console.log(set.size); // 5 (duplicates are removed)
console.log(set.has(3)); // true
set.add(6);
set.delete(1);
console.log([...set]); // [2, 3, 4, 5, 6]

// WeakMap and WeakSet
const weakMap = new WeakMap();
let obj = { name: 'John' };
weakMap.set(obj, 'metadata');
console.log(weakMap.get(obj)); // metadata
// obj = null; // The entry in weakMap will be garbage collected

// Symbol
const uniqueKey = Symbol('description');
const obj2 = {
    [uniqueKey]: 'This is a unique property'
};
console.log(obj2[uniqueKey]); // This is a unique property

// for...of loop
const iterable = [1, 2, 3];
for (const value of iterable) {
    console.log(value); // 1, 2, 3
}

// Object.entries(), Object.values(), Object.keys()
const person = { name: 'John', age: 30, job: 'Developer' };
console.log(Object.keys(person)); // ['name', 'age', 'job']
console.log(Object.values(person)); // ['John', 30, 'Developer']
console.log(Object.entries(person)); // [['name', 'John'], ['age', 30], ['job', 'Developer']]

// Array methods
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.find(n => n > 3)); // 4
console.log(numbers.findIndex(n => n > 3)); // 3
console.log(numbers.includes(3)); // true

// Exponentiation operator
console.log(2 ** 3); // 8

// Optional chaining
const user = {
    profile: {
        address: {
            city: 'New York'
        }
    }
};
console.log(user?.profile?.address?.city); // New York
console.log(user?.settings?.theme); // undefined (no error)

// Nullish coalescing
const value = null;
console.log(value ?? 'default'); // 'default'
console.log(0 ?? 'default'); // 0 (0 is not nullish)
console.log('' ?? 'default'); // '' (empty string is not nullish)

// Logical assignment operators
let a = null;
a ??= 10; // a = a ?? 10
console.log(a); // 10

let b = 5;
b ??= 10;
console.log(b); // 5 (b was not nullish)

let c = 0;
c ||= 10; // c = c || 10
console.log(c); // 10 (0 is falsy)

let d = 5;
d &&= 10; // d = d && 10
console.log(d); // 10

Functional Programming

Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data.

First-Class Functions

In JavaScript, functions are first-class citizens, which means they can be assigned to variables, passed as arguments, and returned from other functions.

// Assigning functions to variables
const add = function(a, b) {
    return a + b;
};

// Passing functions as arguments
function calculate(operation, a, b) {
    return operation(a, b);
}

console.log(calculate(add, 2, 3)); // 5

// Returning functions from functions
function createMultiplier(factor) {
    return function(number) {
        return number * factor;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

Pure Functions

Pure functions always produce the same output for the same input and have no side effects.

// Pure function
function add(a, b) {
    return a + b;
}

// Impure function (depends on external state)
let counter = 0;
function incrementCounter() {
    counter++;
    return counter;
}

// Impure function (modifies its arguments)
function addToArray(arr, item) {
    arr.push(item);
    return arr;
}

// Pure version of addToArray
function pureAddToArray(arr, item) {
    return [...arr, item];
}

const numbers = [1, 2, 3];
const impureResult = addToArray(numbers, 4);
console.log(numbers); // [1, 2, 3, 4] (original array modified)

const numbers2 = [1, 2, 3];
const pureResult = pureAddToArray(numbers2, 4);
console.log(numbers2); // [1, 2, 3] (original array unchanged)
console.log(pureResult); // [1, 2, 3, 4]

Higher-Order Functions

Higher-order functions are functions that take other functions as arguments or return functions as results.

// Higher-order function that takes a function as an argument
function map(arr, fn) {
    const result = [];
    for (let i = 0; i < arr.length; i++) {
        result.push(fn(arr[i]));
    }
    return result;
}

const numbers = [1, 2, 3, 4, 5];
const doubled = map(numbers, n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// Higher-order function that returns a function
function compose(f, g) {
    return function(x) {
        return f(g(x));
    };
}

const addOne = x => x + 1;
const square = x => x * x;
const addOneThenSquare = compose(square, addOne);
const squareThenAddOne = compose(addOne, square);

console.log(addOneThenSquare(2)); // (2 + 1)² = 9
console.log(squareThenAddOne(2)); // 2² + 1 = 5

Array Methods for Functional Programming

JavaScript provides several array methods that support functional programming.

const numbers = [1, 2, 3, 4, 5];

// map - transform each element
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// filter - select elements that match a condition
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4]

// reduce - combine elements into a single value
const sum = numbers.reduce((total, n) => total + n, 0);
console.log(sum); // 15

// find - find the first element that matches a condition
const firstEven = numbers.find(n => n % 2 === 0);
console.log(firstEven); // 2

// every - check if all elements match a condition
const allPositive = numbers.every(n => n > 0);
console.log(allPositive); // true

// some - check if any element matches a condition
const hasEven = numbers.some(n => n % 2 === 0);
console.log(hasEven); // true

// Chaining array methods
const result = numbers
    .filter(n => n % 2 === 0) // [2, 4]
    .map(n => n * 3) // [6, 12]
    .reduce((total, n) => total + n, 0); // 18

console.log(result); // 18

Function Composition and Pipelines

Function composition is a technique for combining multiple functions to create a new function.

// Simple functions
const add10 = x => x + 10;
const multiply2 = x => x * 2;
const subtract5 = x => x - 5;

// Manual composition
const manualComposition = x => subtract5(multiply2(add10(x)));
console.log(manualComposition(5)); // ((5 + 10) * 2) - 5 = 25

// Compose function (right to left)
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);

// Pipe function (left to right)
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);

// Using compose
const composed = compose(subtract5, multiply2, add10);
console.log(composed(5)); // ((5 + 10) * 2) - 5 = 25

// Using pipe
const piped = pipe(add10, multiply2, subtract5);
console.log(piped(5)); // ((5 + 10) * 2) - 5 = 25

// More complex example
const users = [
    { id: 1, name: 'John', age: 30 },
    { id: 2, name: 'Jane', age: 25 },
    { id: 3, name: 'Bob', age: 40 },
    { id: 4, name: 'Alice', age: 35 }
];

// Functions for data transformation
const filterAdults = users => users.filter(user => user.age >= 30);
const sortByAge = users => [...users].sort((a, b) => a.age - b.age);
const mapToNames = users => users.map(user => user.name);

// Using pipe to create a data transformation pipeline
const getAdultNamesSortedByAge = pipe(
    filterAdults,
    sortByAge,
    mapToNames
);

console.log(getAdultNamesSortedByAge(users)); // ['John', 'Alice', 'Bob']

Immutability

Immutability is a core principle of functional programming, where data is never modified after it's created.

// Mutable approach (avoid in functional programming)
const addItemMutable = (cart, item) => {
    cart.push(item);
    return cart;
};

// Immutable approach
const addItemImmutable = (cart, item) => [...cart, item];

// Updating objects immutably
const user = { name: 'John', age: 30 };

// Mutable update (avoid)
const updateAgeMutable = (user, age) => {
    user.age = age;
    return user;
};

// Immutable updates
const updateAgeImmutable1 = (user, age) => ({ ...user, age });
const updateAgeImmutable2 = (user, age) => Object.assign({}, user, { age });

// Nested updates
const state = {
    user: {
        name: 'John',
        address: {
            city: 'New York',
            country: 'USA'
        }
    },
    settings: {
        theme: 'light'
    }
};

// Updating nested properties immutably
const updateCity = (state, city) => ({
    ...state,
    user: {
        ...state.user,
        address: {
            ...state.user.address,
            city
        }
    }
});

const newState = updateCity(state, 'San Francisco');
console.log(state.user.address.city); // New York (unchanged)
console.log(newState.user.address.city); // San Francisco

Currying and Partial Application

Currying is the technique of transforming a function that takes multiple arguments into a sequence of functions that each take a single argument.

// Regular function
function add(a, b, c) {
    return a + b + c;
}

// Curried function
function curriedAdd(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        };
    };
}

// Using the curried function
console.log(curriedAdd(1)(2)(3)); // 6

// Arrow function syntax for currying
const curriedAddArrow = a => b => c => a + b + c;
console.log(curriedAddArrow(1)(2)(3)); // 6

// Curry function that transforms a regular function into a curried one
function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        }
        return function(...moreArgs) {
            return curried.apply(this, args.concat(moreArgs));
        };
    };
}

const curriedAdd2 = curry(add);
console.log(curriedAdd2(1)(2)(3)); // 6
console.log(curriedAdd2(1, 2)(3)); // 6
console.log(curriedAdd2(1)(2, 3)); // 6
console.log(curriedAdd2(1, 2, 3)); // 6

// Partial application
function partial(fn, ...args) {
    return function(...moreArgs) {
        return fn.apply(this, args.concat(moreArgs));
    };
}

const add5 = partial(add, 5);
console.log(add5(10, 20)); // 35

// Practical example: filtering with currying
const filter = curry(function(predicate, array) {
    return array.filter(predicate);
});

const isEven = x => x % 2 === 0;
const filterEven = filter(isEven);

console.log(filterEven([1, 2, 3, 4, 5])); // [2, 4]

Recursion and Tail Call Optimization

Recursion is a technique where a function calls itself to solve a problem.

// Simple recursion: factorial
function factorial(n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

console.log(factorial(5)); // 120

// Tail-recursive factorial
function factorialTail(n, acc = 1) {
    if (n <= 1) return acc;
    return factorialTail(n - 1, n * acc);
}

console.log(factorialTail(5)); // 120

// Fibonacci sequence
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

console.log(fibonacci(10)); // 55

// Tail-recursive fibonacci
function fibonacciTail(n, a = 0, b = 1) {
    if (n === 0) return a;
    if (n === 1) return b;
    return fibonacciTail(n - 1, b, a + b);
}

console.log(fibonacciTail(10)); // 55

// Recursive tree traversal
const tree = {
    value: 1,
    left: {
        value: 2,
        left: { value: 4, left: null, right: null },
        right: { value: 5, left: null, right: null }
    },
    right: {
        value: 3,
        left: { value: 6, left: null, right: null },
        right: { value: 7, left: null, right: null }
    }
};

// In-order traversal
function inOrderTraversal(node, result = []) {
    if (node === null) return result;
    
    inOrderTraversal(node.left, result);
    result.push(node.value);
    inOrderTraversal(node.right, result);
    
    return result;
}

console.log(inOrderTraversal(tree)); // [4, 2, 5, 1, 6, 3, 7]

Functional Libraries

There are several libraries that support functional programming in JavaScript.

// Lodash/fp
// import _ from 'lodash/fp';

// const users = [
//     { id: 1, name: 'John', age: 30 },
//     { id: 2, name: 'Jane', age: 25 },
//     { id: 3, name: 'Bob', age: 40 }
// ];

// const getOldestUser = _.flow(
//     _.filter(user => user.age >= 30),
//     _.maxBy('age'),
//     _.get('name')
// );

// console.log(getOldestUser(users)); // 'Bob'

// Ramda
// import * as R from 'ramda';

// const multiply = R.curry((a, b) => a * b);
// const double = multiply(2);
// const triple = multiply(3);

// console.log(double(5)); // 10
// console.log(triple(5)); // 15

// const getAdultNames = R.pipe(
//     R.filter(R.propSatisfies(age => age >= 30, 'age')),
//     R.sortBy(R.prop('age')),
//     R.map(R.prop('name'))
// );

// console.log(getAdultNames(users)); // ['John', 'Bob']

// Implementing your own functional utilities
const map = fn => array => array.map(fn);
const filter = fn => array => array.filter(fn);
const reduce = (fn, initial) => array => array.reduce(fn, initial);
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);

const users = [
    { id: 1, name: 'John', age: 30 },
    { id: 2, name: 'Jane', age: 25 },
    { id: 3, name: 'Bob', age: 40 }
];

const isAdult = user => user.age >= 30;
const getName = user => user.name;

const getAdultNames = pipe(
    filter(isAdult),
    map(getName)
);

console.log(getAdultNames(users)); // ['John', 'Bob']

Continue to Part 2

Continue to Part 2 to learn about Asynchronous JavaScript, Modules and Bundlers, Design Patterns, and Testing and Debugging.