JavaScript Functions and Functional Programming

1. Function Declaration vs Function Expression Syntax

Function Type Syntax Hoisting Use Case
Function Declaration function name(params) { body } ✓ Fully hoisted (can call before declaration) Top-level functions, utility functions requiring hoisting
Function Expression const name = function(params) { body }; ✗ Variable hoisted but undefined (TDZ for let/const) Conditional function creation, callbacks, function as data
Named Function Expression const f = function name(params) { body }; ✗ Not hoisted; name only visible inside function Recursion in expressions, better stack traces
Arrow Function ES6 const name = (params) => expression ✗ Variable hoisting behavior Short callbacks, lexical this binding, concise syntax
Method Definition ES6 { methodName(params) { body } } ✗ Part of object/class definition Object methods, class methods with concise syntax
Generator Function ES6 function* name(params) { yield value } ✓ Hoisted like regular functions Iterators, lazy evaluation, pausable execution
Async Function ES8 async function name(params) { await promise } ✓ Hoisted (declaration) / ✗ (expression) Asynchronous operations with cleaner syntax

Example: Function declaration vs expression

// Function Declaration - hoisted, can call before definition
console.log(add(2, 3));  // Works: 5
function add(a, b) {
    return a + b;
}

// Function Expression - not hoisted
// console.log(subtract(5, 2));  // Error: Cannot access before initialization
const subtract = function(a, b) {
    return a - b;
};

// Named Function Expression - name visible only inside
const factorial = function fact(n) {
    return n <= 1 ? 1 : n * fact(n - 1);  // 'fact' accessible here
};
console.log(factorial(5));  // 120
// console.log(fact(5));  // Error: fact is not defined

// Arrow function - concise syntax
const multiply = (a, b) => a * b;
const square = x => x * x;  // Single param, no parens needed
const greet = () => 'Hello';  // No params
Best Practice: Use function declarations for top-level utility functions that need hoisting. Use arrow functions for callbacks and methods that don't need their own this. Use function expressions when functions are conditional or need to be treated as values.

2. Arrow Functions (=>) and Lexical this Binding

Feature Arrow Function Regular Function Impact
this Binding Lexical (from enclosing scope) Dynamic (based on call) No .bind() needed for callbacks
arguments Object ✗ Not available (use rest params) ✓ Available Use ...args for arrow functions
new Operator ✗ Cannot be constructor ✓ Can be constructor Arrow functions have no [[Construct]]
prototype Property ✗ No prototype ✓ Has prototype Lighter memory footprint for arrows
super Keyword ✗ Cannot use super ✓ Can use in methods Use regular functions for class methods needing super
yield Keyword ✗ Cannot be generator ✓ Can yield Use function* for generators
Syntax Form Example When to Use
No Parameters () => expression Functions with no inputs
Single Parameter x => expression Unary functions (map, filter callbacks)
Multiple Parameters (x, y) => expression Binary/multi-parameter functions
Expression Body x => x * 2 Single expression, implicit return
Block Body x => { return x * 2; } Multiple statements, explicit return
Object Literal Return x => ({ value: x }) Returning object (wrap in parentheses)

Example: Lexical this binding in practice

// Problem with regular functions - this binding changes
function Timer() {
    this.seconds = 0;
    
    // Regular function - 'this' is window/undefined in strict mode
    setInterval(function() {
        this.seconds++;  // ✗ Wrong 'this'
        console.log(this.seconds);  // NaN
    }, 1000);
}

// Solution 1: Arrow function - lexical 'this'
function Timer() {
    this.seconds = 0;
    
    setInterval(() => {
        this.seconds++;  // ✓ Correct 'this' from Timer
        console.log(this.seconds);  // 1, 2, 3...
    }, 1000);
}

// Solution 2: Old approach with .bind() (before ES6)
function Timer() {
    this.seconds = 0;
    setInterval(function() {
        this.seconds++;
    }.bind(this), 1000);  // Explicitly bind 'this'
}

// Class methods with arrow functions
class Counter {
    count = 0;
    
    // Arrow function as class field - 'this' always bound to instance
    increment = () => {
        this.count++;
    }
    
    // Regular method - 'this' depends on how it's called
    decrement() {
        this.count--;
    }
}

const counter = new Counter();
const inc = counter.increment;
const dec = counter.decrement;

inc();  // ✓ Works - arrow function has lexical 'this'
dec();  // ✗ Error - 'this' is undefined (not bound)

Example: Arrow function syntax variations

// Expression body - implicit return
const double = x => x * 2;
const sum = (a, b) => a + b;

// Block body - explicit return needed
const greet = name => {
    const message = `Hello, ${name}!`;
    return message.toUpperCase();
};

// Returning object literal - wrap in parentheses
const makePerson = (name, age) => ({ name, age });
const point = (x, y) => ({ x: x, y: y });

// Array methods with arrows
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const total = numbers.reduce((sum, n) => sum + n, 0);

// Chaining with concise arrows
const result = [1, 2, 3, 4, 5]
    .filter(x => x % 2 === 0)
    .map(x => x * x)
    .reduce((sum, x) => sum + x, 0);  // 20
Warning: Don't use arrow functions as object methods or event handlers where you need dynamic this. Arrow functions cannot be used as constructors with new. They don't have arguments object - use rest parameters instead.

3. Function Parameters (default, rest, destructuring)

Parameter Type Syntax Description Example
Required Parameters function f(a, b) Traditional positional parameters; undefined if not provided f(1, 2)
Default Parameters ES6 function f(a = 0, b = 1) Provides default value if undefined (not for null) f() // a=0, b=1
Rest Parameters ES6 function f(...args) Collects remaining arguments into array; must be last f(1,2,3) // args=[1,2,3]
Destructuring (Array) ES6 function f([a, b]) Extracts values from array argument by position f([10, 20])
Destructuring (Object) ES6 function f({x, y}) Extracts properties from object argument by name f({x:1, y:2})
Nested Destructuring ES6 function f({a: {b}}) Extracts nested properties from deep object structure f({a: {b: 5}})
Default + Destructuring ES6 function f({x = 0} = {}) Combines destructuring with defaults for object properties f() // x=0
Pattern Example Behavior
Default with Expression f(a = computeDefault()) Expression evaluated only if parameter is undefined
Using Previous Params f(a, b = a * 2) Default can reference earlier parameters
Rest with Destructuring f([first, ...rest]) Combine array destructuring with rest collection
Rename in Destructuring f({x: width, y: height}) Extract and rename properties simultaneously

Example: Default parameters

// Basic default parameters
function greet(name = 'Guest', greeting = 'Hello') {
    return `${greeting}, ${name}!`;
}
console.log(greet());  // "Hello, Guest!"
console.log(greet('Alice'));  // "Hello, Alice!"
console.log(greet('Bob', 'Hi'));  // "Hi, Bob!"

// Default only applies to undefined, not null
console.log(greet(null));  // "Hello, null!" (null !== undefined)
console.log(greet(undefined, 'Hey'));  // "Hey, Guest!" (undefined triggers default)

// Default expressions evaluated at call time
let counter = 0;
function f(x = counter++) {
    return x;
}
console.log(f());  // 0 (counter becomes 1)
console.log(f());  // 1 (counter becomes 2)
console.log(f(10));  // 10 (default not used, counter stays 2)

// Using previous parameters in defaults
function createRect(width, height = width) {
    return { width, height };  // height defaults to width (square)
}
console.log(createRect(5));  // { width: 5, height: 5 }
console.log(createRect(5, 10));  // { width: 5, height: 10 }

Example: Rest parameters

// Rest parameters collect remaining arguments into array
function sum(...numbers) {
    return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4));  // 10
console.log(sum());  // 0 (empty array)

// Combine regular params with rest
function multiply(multiplier, ...numbers) {
    return numbers.map(n => n * multiplier);
}
console.log(multiply(2, 1, 2, 3));  // [2, 4, 6]

// Rest must be last parameter
// function invalid(...args, last) {}  // ✗ SyntaxError

// Rest vs arguments object
function oldWay() {
    // arguments is array-like, not real array
    const args = Array.from(arguments);
    return args.reduce((sum, n) => sum + n, 0);
}

function newWay(...args) {
    // args is real array with all array methods
    return args.reduce((sum, n) => sum + n, 0);
}

// Arrow functions don't have arguments, use rest
const arrowSum = (...nums) => nums.reduce((s, n) => s + n, 0);

Example: Destructuring parameters

// Object destructuring in parameters
function createUser({ name, age, email = 'none@example.com' }) {
    return { name, age, email };
}
createUser({ name: 'Alice', age: 30 });
// { name: 'Alice', age: 30, email: 'none@example.com' }

// Array destructuring in parameters
function sumFirstTwo([a, b]) {
    return a + b;
}
console.log(sumFirstTwo([10, 20, 30]));  // 30

// Nested destructuring
function displayPoint({ coords: { x, y }, label = 'Point' }) {
    return `${label}: (${x}, ${y})`;
}
displayPoint({ coords: { x: 10, y: 20 } });  // "Point: (10, 20)"

// Destructuring with rest
function getFirstAndRest([first, ...rest]) {
    return { first, rest };
}
console.log(getFirstAndRest([1, 2, 3, 4]));
// { first: 1, rest: [2, 3, 4] }

// Rename while destructuring
function printDimensions({ width: w, height: h }) {
    console.log(`Width: ${w}, Height: ${h}`);
}
printDimensions({ width: 100, height: 200 });

// Default for entire destructured object
function config({ host = 'localhost', port = 8080 } = {}) {
    return `${host}:${port}`;
}
console.log(config());  // "localhost:8080"
console.log(config({ host: 'example.com' }));  // "example.com:8080"

4. Higher-Order Functions and Callbacks

Concept Definition Common Use Cases
Higher-Order Function (HOF) Function that takes function(s) as argument(s) and/or returns a function map, filter, reduce, addEventListener, middleware
Callback Function Function passed as argument to be executed later (synchronous or asynchronous) Event handlers, async operations, array methods
Function Returning Function HOF that creates and returns new function (function factory) Configuration, currying, closures, partial application
Function Composition Combining functions where output of one is input to another Data transformation pipelines, functional programming
Built-in HOF Purpose Callback Signature Returns
Array.map() Transform each element (element, index, array) => newValue New array (same length)
Array.filter() Select elements by condition (element, index, array) => boolean New array (≤ original length)
Array.reduce() Aggregate to single value (accumulator, element, index, array) => newAcc Single accumulated value
Array.forEach() Execute side effects (element, index, array) => void undefined (no return value)
Array.find() Find first matching element (element, index, array) => boolean Element or undefined
Array.some() Test if any element passes (element, index, array) => boolean boolean
Array.every() Test if all elements pass (element, index, array) => boolean boolean

Example: Higher-order functions

// Function that takes function as argument
function repeat(n, action) {
    for (let i = 0; i < n; i++) {
        action(i);
    }
}
repeat(3, console.log);  // Logs: 0, 1, 2

// Function that returns function (function factory)
function multiplier(factor) {
    return function(number) {
        return number * factor;
    };
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5));  // 10
console.log(triple(5));  // 15

// Function composition
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);
console.log(addOneThenSquare(4));  // 25 (5²)

// Practical HOF: logger wrapper
function withLogging(fn) {
    return function(...args) {
        console.log(`Calling ${fn.name} with`, args);
        const result = fn(...args);
        console.log(`Result:`, result);
        return result;
    };
}
const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3);  // Logs call info, returns 5

Example: Array HOF methods in action

const users = [
    { id: 1, name: 'Alice', age: 25, active: true },
    { id: 2, name: 'Bob', age: 30, active: false },
    { id: 3, name: 'Charlie', age: 35, active: true }
];

// map - transform data
const names = users.map(user => user.name);
// ['Alice', 'Bob', 'Charlie']

// filter - select subset
const activeUsers = users.filter(user => user.active);
// [Alice, Charlie]

// reduce - aggregate
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
// 90

// Chaining HOFs
const result = users
    .filter(user => user.active)
    .map(user => user.name)
    .map(name => name.toUpperCase());
// ['ALICE', 'CHARLIE']

// find - first match
const user = users.find(u => u.age > 25);
// { id: 2, name: 'Bob', age: 30, active: false }

// some - any match
const hasInactive = users.some(u => !u.active);  // true

// every - all match
const allActive = users.every(u => u.active);  // false

// Complex reduce example
const grouped = users.reduce((groups, user) => {
    const key = user.active ? 'active' : 'inactive';
    groups[key] = groups[key] || [];
    groups[key].push(user);
    return groups;
}, {});
// { active: [Alice, Charlie], inactive: [Bob] }
Best Practice: Prefer declarative HOFs (map, filter, reduce) over imperative loops for data transformation. Chain operations for readability. Use arrow functions for concise callbacks. Remember that HOFs enable code reuse and abstraction.

5. Closures and Function Scope Management

Concept Definition Key Characteristics
Closure Function that retains access to variables from its outer (enclosing) scope even after outer function has returned Lexical scoping, persistent state, data privacy
Lexical Environment Internal structure holding variable bindings and reference to outer environment Created at function execution, forms scope chain
Scope Chain Chain of lexical environments searched for variable resolution Inner to outer traversal, stops at first match
Private Variables Variables accessible only through closures, not directly from outside Encapsulation, data hiding, module pattern
Closure Pattern Use Case Example Pattern
Function Factory Create specialized functions with preset parameters function makeAdder(x) { return y => x + y; }
Private State Encapsulate data with controlled access function counter() { let n=0; return ()=>++n; }
Module Pattern Create modules with public/private members IIFE returning object with methods accessing private vars
Event Handlers Preserve context in async callbacks Handler functions accessing outer scope variables
Memoization Cache function results using closure-stored cache Closure over cache object for recursive functions

Example: Basic closure mechanics

// Simple closure - inner function accesses outer variable
function outer() {
    let count = 0;  // Variable in outer scope
    
    function inner() {
        count++;  // Accesses count from outer scope
        return count;
    }
    
    return inner;
}

const counter = outer();
console.log(counter());  // 1
console.log(counter());  // 2
console.log(counter());  // 3
// 'count' persists between calls through closure

// Multiple closures over same variable
function makeCounter() {
    let count = 0;
    return {
        increment: () => ++count,
        decrement: () => --count,
        value: () => count
    };
}

const c1 = makeCounter();
const c2 = makeCounter();  // Separate closure, separate count
c1.increment();
c1.increment();
console.log(c1.value());  // 2
console.log(c2.value());  // 0 (independent)

Example: Practical closure patterns

// Function factory with closure
function multiplier(factor) {
    return function(number) {
        return number * factor;  // 'factor' remembered via closure
    };
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5));  // 10
console.log(triple(5));  // 15

// Private variables pattern
function createBankAccount(initialBalance) {
    let balance = initialBalance;  // Private variable
    
    return {
        deposit(amount) {
            if (amount > 0) balance += amount;
            return balance;
        },
        withdraw(amount) {
            if (amount > 0 && amount <= balance) balance -= amount;
            return balance;
        },
        getBalance() {
            return balance;
        }
    };
    // No direct access to 'balance' from outside
}

const account = createBankAccount(100);
account.deposit(50);  // 150
account.withdraw(30);  // 120
console.log(account.balance);  // undefined (private)
console.log(account.getBalance());  // 120

// Memoization with closure
function memoize(fn) {
    const cache = {};  // Closure over cache
    return function(...args) {
        const key = JSON.stringify(args);
        if (key in cache) {
            console.log('Cached');
            return cache[key];
        }
        const result = fn(...args);
        cache[key] = result;
        return result;
    };
}

const slowFunction = (n) => {
    console.log('Computing...');
    return n * 2;
};
const fast = memoize(slowFunction);
fast(5);  // Computing... 10
fast(5);  // Cached 10
Common Pitfall: Closures in loops. Using var creates single binding shared across iterations. Solution: use let (creates new binding per iteration) or IIFE to capture value.
// ✗ Problem with var
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100);
}
// Logs: 3, 3, 3 (all closures see same 'i')

// ✓ Solution 1: Use let
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100);
}
// Logs: 0, 1, 2 (each closure gets own 'i')

// ✓ Solution 2: IIFE (old approach)
for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(() => console.log(j), 100);
    })(i);
}
// Logs: 0, 1, 2

6. IIFE Patterns (Immediately Invoked Function Expression) and Module Creation

Pattern Syntax Purpose Use Case
Basic IIFE (function() { })(); Execute function immediately, create private scope Avoid global pollution, one-time initialization
Alternative Syntax (function() { }()); Same as basic IIFE (different grouping) Style preference, same behavior
Arrow IIFE ES6 (() => { })(); Concise IIFE with lexical this Modern alternative, shorter syntax
Named IIFE (function name() { })(); Self-reference for recursion, better debugging Recursive IIFEs, stack traces
IIFE with Parameters (function(x) { })(value); Pass values into IIFE scope Dependency injection, value capture
IIFE Returning Value const x = (function() { return val; })(); Initialize variable with complex logic Complex initialization, module pattern
Module Pattern (IIFE) const mod = (function() { return {...}; })(); Create module with public API, private state Pre-ES6 modules, encapsulation

Example: IIFE basics and variations

// Basic IIFE - executes immediately
(function() {
    const message = 'Hello from IIFE';
    console.log(message);
})();  // Logs: Hello from IIFE
// 'message' not accessible here (private scope)

// IIFE with parameters
(function(name) {
    console.log(`Hello, ${name}!`);
})('Alice');  // Hello, Alice!

// IIFE returning value
const config = (function() {
    const privateKey = 'secret123';
    return {
        apiUrl: 'https://api.example.com',
        timeout: 5000,
        getKey: () => privateKey  // Controlled access
    };
})();
console.log(config.apiUrl);  // 'https://api.example.com'
console.log(config.privateKey);  // undefined (private)

// Arrow function IIFE (ES6+)
(() => {
    const temp = 'temporary variable';
    console.log(temp);
})();

// Named IIFE for recursion
(function factorial(n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
})(5);  // 120

// IIFE for variable capture in loops (pre-let solution)
for (var i = 0; i < 3; i++) {
    (function(index) {
        setTimeout(() => console.log(index), 100);
    })(i);  // Capture current value of i
}

Example: Module pattern with IIFE

// Classic Module Pattern
const Calculator = (function() {
    // Private variables and functions
    let history = [];
    
    function log(operation, result) {
        history.push({ operation, result, timestamp: Date.now() });
    }
    
    // Public API
    return {
        add(a, b) {
            const result = a + b;
            log('add', result);
            return result;
        },
        subtract(a, b) {
            const result = a - b;
            log('subtract', result);
            return result;
        },
        getHistory() {
            return [...history];  // Return copy, not reference
        },
        clearHistory() {
            history = [];
        }
    };
})();

Calculator.add(5, 3);  // 8
Calculator.subtract(10, 4);  // 6
console.log(Calculator.getHistory());  // Array of operations
console.log(Calculator.history);  // undefined (private)

// Revealing Module Pattern
const Counter = (function() {
    let count = 0;
    
    function increment() { return ++count; }
    function decrement() { return --count; }
    function getValue() { return count; }
    function reset() { count = 0; }
    
    // Reveal selected functions
    return {
        increment,
        decrement,
        getValue,
        reset
    };
})();

Counter.increment();  // 1
Counter.increment();  // 2
console.log(Counter.getValue());  // 2

// Module with dependencies
const UserManager = (function($, _) {
    // Use jQuery ($) and lodash (_) passed as parameters
    function getUsers() {
        return _.map(users, user => user.name);
    }
    
    return { getUsers };
})(jQuery, lodash);  // Inject dependencies
Modern Alternative: ES6 modules (import/export) have largely replaced IIFE-based modules. However, IIFEs remain useful for: creating private scope, avoiding global pollution, one-time initialization code, and browser compatibility when modules aren't available.

7. Function Methods (call, apply, bind)

Method Syntax Arguments Returns When Called
call() fn.call(thisArg, arg1, arg2, ...) Individual arguments Function result Immediately
apply() fn.apply(thisArg, [args]) Array of arguments Function result Immediately
bind() fn.bind(thisArg, arg1, ...) Individual arguments (partial) New bound function Later (returned fn)
Use Case Best Method Why
Set 'this' and call now call() Known number of arguments, immediate execution
Set 'this' with array args apply() Arguments already in array, or use Math.max/min with arrays
Set 'this' for later use bind() Event handlers, callbacks, partial application
Partial application bind() Pre-fill some arguments, returns reusable function
Borrowing methods call() or apply() Use array methods on array-like objects

Example: call() and apply()

const person = {
    name: 'Alice',
    greet(greeting, punctuation) {
        return `${greeting}, I'm ${this.name}${punctuation}`;
    }
};

console.log(person.greet('Hello', '!'));  // "Hello, I'm Alice!"

// call() - set 'this' and pass individual arguments
const otherPerson = { name: 'Bob' };
console.log(person.greet.call(otherPerson, 'Hi', '.'));
// "Hi, I'm Bob."

// apply() - set 'this' and pass arguments as array
console.log(person.greet.apply(otherPerson, ['Hey', '...']));
// "Hey, I'm Bob..."

// Practical: borrowing array methods for array-like objects
function sumAll() {
    // 'arguments' is array-like, not real array
    // Borrow reduce() from Array
    return Array.prototype.reduce.call(arguments, (sum, n) => sum + n, 0);
}
console.log(sumAll(1, 2, 3, 4));  // 10

// Math.max with apply (pre-spread operator)
const numbers = [5, 2, 9, 1, 7];
const max = Math.max.apply(null, numbers);  // 9
// Modern: Math.max(...numbers)

// Invoking function with specific context
function introduce() {
    return `My name is ${this.name} and I'm ${this.age}`;
}
const user = { name: 'Charlie', age: 30 };
console.log(introduce.call(user));
// "My name is Charlie and I'm 30"

Example: bind() for permanent binding

const obj = {
    value: 42,
    getValue() {
        return this.value;
    }
};

// Problem: 'this' lost when method assigned to variable
const getValue = obj.getValue;
console.log(getValue());  // undefined ('this' is window/undefined)

// Solution: bind() creates new function with fixed 'this'
const boundGetValue = obj.getValue.bind(obj);
console.log(boundGetValue());  // 42

// Practical: event handlers
class Button {
    constructor(label) {
        this.label = label;
        this.clickCount = 0;
    }
    
    handleClick() {
        this.clickCount++;
        console.log(`${this.label} clicked ${this.clickCount} times`);
    }
}

const btn = new Button('Submit');
// Without bind, 'this' would be the button element, not our object
document.querySelector('button').addEventListener('click', 
    btn.handleClick.bind(btn)
);

// Partial application with bind()
function multiply(a, b) {
    return a * b;
}
const double = multiply.bind(null, 2);  // Pre-fill first argument
const triple = multiply.bind(null, 3);
console.log(double(5));  // 10
console.log(triple(5));  // 15

// Binding with setTimeout
const timer = {
    seconds: 0,
    start() {
        // bind() ensures 'this' refers to timer object
        setInterval(function() {
            this.seconds++;
            console.log(this.seconds);
        }.bind(this), 1000);
        
        // Modern alternative: arrow function (lexical 'this')
        // setInterval(() => { this.seconds++; }, 1000);
    }
};

// Multiple bind() calls - first one wins
function log() { console.log(this.value); }
const obj1 = { value: 1 };
const obj2 = { value: 2 };
const bound1 = log.bind(obj1);
const bound2 = bound1.bind(obj2);  // Has no effect
bound2();  // 1 (not 2 - can't rebind)
Modern Context: Arrow functions (=>) with lexical this have reduced the need for bind() in many cases. However, call(), apply(), and bind() remain essential for: method borrowing, explicit context control, partial application, and working with legacy code.

8. Recursion and Tail Call Optimization

Concept Definition Characteristics
Recursion Function calling itself to solve smaller subproblems Base case + recursive case; elegant for tree/graph problems
Base Case Condition that stops recursion without further calls Prevents infinite recursion; returns direct result
Recursive Case Logic that breaks problem down and calls function recursively Progress toward base case; must modify parameters
Call Stack Stack storing execution contexts for function calls Each recursive call adds frame; limited size (stack overflow risk)
Tail Call Function call that's the last operation (nothing after return) Eligible for optimization; no pending operations
Tail Call Optimization (TCO) LIMITED Reuse stack frame for tail calls instead of creating new ones ES6 spec, but poor browser support; prevents stack overflow
Pattern Stack Usage TCO Eligible When to Use
Standard Recursion O(n) stack frames ✗ No (pending operations after call) Simple problems, small input size
Tail Recursion O(1) with TCO, O(n) without ✓ Yes (return is direct recursive call) Large inputs if TCO available
Accumulator Pattern O(1) with TCO ✓ Yes (pass result through parameters) Convert standard recursion to tail recursion
Iteration (Loop) O(1) stack N/A (no recursion) Production code, guaranteed stack safety

Example: Basic recursion patterns

// Standard recursion - NOT tail recursive
function factorial(n) {
    // Base case
    if (n <= 1) return 1;
    
    // Recursive case - operation AFTER recursive call
    return n * factorial(n - 1);  // ✗ Not tail call (multiplication pending)
}
console.log(factorial(5));  // 120
// Stack: factorial(5) waits for factorial(4)
//        factorial(4) waits for factorial(3), etc.

// Tail recursive version - uses accumulator
function factorialTail(n, acc = 1) {
    // Base case
    if (n <= 1) return acc;
    
    // Recursive case - nothing after recursive call
    return factorialTail(n - 1, n * acc);  // ✓ Tail call (direct return)
}
console.log(factorialTail(5));  // 120
// With TCO: reuses same stack frame

// Fibonacci - standard recursion (exponential time!)
function fib(n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);  // Two recursive calls
}
// Very slow for large n: fib(40) takes seconds

// Fibonacci - tail recursive with two accumulators
function fibTail(n, a = 0, b = 1) {
    if (n === 0) return a;
    return fibTail(n - 1, b, a + b);  // Tail call
}
console.log(fibTail(40));  // Fast even for large n

// Array sum - standard recursion
function sumArray(arr) {
    if (arr.length === 0) return 0;
    return arr[0] + sumArray(arr.slice(1));  // ✗ Not tail call
}

// Array sum - tail recursive
function sumArrayTail(arr, acc = 0) {
    if (arr.length === 0) return acc;
    return sumArrayTail(arr.slice(1), acc + arr[0]);  // ✓ Tail call
}

Example: Converting to tail recursion

// Problem: countdown - NOT tail recursive
function countdown(n) {
    if (n < 0) return;
    console.log(n);
    countdown(n - 1);
    console.log('Done', n);  // ✗ Operation after recursive call
}

// Solution: tail recursive countdown
function countdownTail(n) {
    if (n < 0) return;
    console.log(n);
    return countdownTail(n - 1);  // ✓ Tail call
}

// Problem: power - NOT tail recursive
function power(base, exp) {
    if (exp === 0) return 1;
    return base * power(base, exp - 1);  // ✗ Multiplication pending
}

// Solution: tail recursive with accumulator
function powerTail(base, exp, acc = 1) {
    if (exp === 0) return acc;
    return powerTail(base, exp - 1, acc * base);  // ✓ Tail call
}

// Tree traversal - naturally recursive
function sumTree(node) {
    if (!node) return 0;
    return node.value 
        + sumTree(node.left) 
        + sumTree(node.right);
    // Not tail recursive but appropriate for trees
}

// Tail recursive tree traversal (using continuation)
function sumTreeTail(node, cont = x => x) {
    if (!node) return cont(0);
    return sumTreeTail(node.left, leftSum =>
        sumTreeTail(node.right, rightSum =>
            cont(node.value + leftSum + rightSum)
        )
    );
}
Important Limitations:
  • TCO Support: Proper tail call optimization is in ES6 spec but only Safari implements it. Chrome/Firefox/Edge don't support TCO.
  • Stack Overflow Risk: Without TCO, even tail-recursive functions will overflow stack with large inputs (typically 10,000-15,000 calls).
  • Production Code: For critical code, use iteration (loops) instead of recursion to guarantee stack safety.
  • Trampoline: Workaround pattern to simulate TCO, but adds complexity.

Example: When to use recursion vs iteration

// ✓ Good use of recursion - tree/graph structures
function findNode(tree, value) {
    if (!tree) return null;
    if (tree.value === value) return tree;
    return findNode(tree.left, value) || findNode(tree.right, value);
}

// ✓ Good use of recursion - divide and conquer
function binarySearch(arr, target, left = 0, right = arr.length - 1) {
    if (left > right) return -1;
    const mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) return mid;
    if (arr[mid] > target) return binarySearch(arr, target, left, mid - 1);
    return binarySearch(arr, target, mid + 1, right);
}

// ✗ Bad use of recursion - simple iteration better
function sumToN(n) {
    if (n === 0) return 0;
    return n + sumToN(n - 1);  // Just use n * (n + 1) / 2
}

// ✓ Better: iterative solution
function sumToNIterative(n) {
    let sum = 0;
    for (let i = 1; i <= n; i++) {
        sum += i;
    }
    return sum;
    // Or: return n * (n + 1) / 2;
}

// Trampoline pattern (TCO workaround)
function trampoline(fn) {
    while (typeof fn === 'function') {
        fn = fn();  // Keep calling until not a function
    }
    return fn;
}

function factorialTrampoline(n, acc = 1) {
    if (n <= 1) return acc;
    return () => factorialTrampoline(n - 1, n * acc);  // Return thunk
}

console.log(trampoline(factorialTrampoline(100000)));  // No stack overflow

Section 6 Summary

  • Function types: Declarations (hoisted), expressions (not hoisted), arrows (lexical this), generators (yield), async (await)
  • Arrow functions: Concise syntax, lexical this, no arguments/new/prototype, ideal for callbacks
  • Parameters: Default values, rest (...args), destructuring (array/object), combine for flexible APIs
  • Higher-order functions: Take/return functions; enables map/filter/reduce, composition, abstraction
  • Closures: Functions retaining outer scope access; enables private state, factories, memoization
  • IIFE: Immediately-invoked functions; creates private scope, module pattern (pre-ES6 modules)
  • call/apply/bind: Control this context; call/apply execute now, bind returns new function
  • Recursion: Self-calling functions; elegant for trees/graphs; tail recursion + TCO prevents stack overflow (limited support)