Advanced Functions

60 min read Intermediate

Dive deep into closures, higher-order functions, callbacks, and other powerful function concepts that make JavaScript so expressive.

Closures

A closure is a function that has access to variables from its outer (enclosing) scope, even after that outer function has returned. This is one of JavaScript's most powerful features.

JavaScript
// Basic closure example
function createGreeting(greeting) {
    // 'greeting' is enclosed in the returned function
    return function(name) {
        return `${greeting}, ${name}!`;
    };
}

const sayHello = createGreeting("Hello");
const sayHi = createGreeting("Hi");

console.log(sayHello("Alice")); // "Hello, Alice!"
console.log(sayHi("Bob"));      // "Hi, Bob!"

// Each function remembers its own 'greeting' value

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

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

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

Practical Closure Examples

JavaScript
// Function factory - create specialized functions
function multiply(factor) {
    return (number) => number * factor;
}

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

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

// Memoization - caching function results
function memoize(fn) {
    const cache = {};
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (key in cache) {
            console.log("Returning cached result");
            return cache[key];
        }
        
        console.log("Computing result");
        const result = fn(...args);
        cache[key] = result;
        return result;
    };
}

const expensiveOperation = (n) => {
    // Simulate heavy computation
    return n * n;
};

const memoizedOperation = memoize(expensiveOperation);

console.log(memoizedOperation(5)); // Computing result -> 25
console.log(memoizedOperation(5)); // Returning cached result -> 25
console.log(memoizedOperation(3)); // Computing result -> 9

// Rate limiter using closure
function createRateLimiter(limit, interval) {
    let calls = 0;
    
    setInterval(() => {
        calls = 0;
    }, interval);
    
    return function(fn) {
        if (calls < limit) {
            calls++;
            return fn();
        }
        console.log("Rate limit exceeded");
        return null;
    };
}

Closure Memory

Closures keep references to variables, not copies. They can access the current value of enclosed variables, and those variables persist in memory as long as the closure exists.

Higher-Order Functions

A higher-order function is a function that takes one or more functions as arguments, returns a function, or both. They enable powerful abstraction patterns.

JavaScript
// Function that takes a function as argument
function repeat(n, action) {
    for (let i = 0; i < n; i++) {
        action(i);
    }
}

repeat(3, console.log);
// 0
// 1
// 2

repeat(5, (i) => console.log(`Step ${i + 1}`));
// Step 1
// Step 2
// ... Step 5

// Function that returns a function
function greaterThan(n) {
    return (m) => m > n;
}

const greaterThan10 = greaterThan(10);
console.log(greaterThan10(15)); // true
console.log(greaterThan10(5));  // false

// Function that does both
function unless(condition, action) {
    if (!condition) {
        action();
    }
}

repeat(5, (n) => {
    unless(n % 2 === 1, () => {
        console.log(n, "is even");
    });
});
// 0 is even
// 2 is even
// 4 is even

// Composing functions
function compose(...functions) {
    return function(x) {
        return functions.reduceRight((acc, fn) => fn(acc), x);
    };
}

const addOne = (x) => x + 1;
const double = (x) => x * 2;
const square = (x) => x * x;

const composed = compose(square, double, addOne);
console.log(composed(3)); // ((3 + 1) * 2)² = 64

// Pipe (opposite order of compose)
function pipe(...functions) {
    return function(x) {
        return functions.reduce((acc, fn) => fn(acc), x);
    };
}

const piped = pipe(addOne, double, square);
console.log(piped(3)); // ((3 + 1) * 2)² = 64

Callbacks

A callback is a function passed as an argument to another function, to be executed later. Callbacks are fundamental to asynchronous programming in JavaScript.

JavaScript
// Simple callback example
function processUserData(name, callback) {
    console.log("Processing data for:", name);
    callback(name);
}

processUserData("Alice", (name) => {
    console.log(`Welcome, ${name}!`);
});

// Callback with error handling pattern
function divide(a, b, successCallback, errorCallback) {
    if (b === 0) {
        errorCallback(new Error("Cannot divide by zero"));
    } else {
        successCallback(a / b);
    }
}

divide(10, 2,
    (result) => console.log("Result:", result),
    (error) => console.log("Error:", error.message)
);
// Output: Result: 5

divide(10, 0,
    (result) => console.log("Result:", result),
    (error) => console.log("Error:", error.message)
);
// Output: Error: Cannot divide by zero

// Simulating async operation with callback
function fetchData(id, callback) {
    console.log("Fetching data...");
    
    setTimeout(() => {
        const data = { id: id, name: "User " + id };
        callback(null, data); // Error-first callback pattern
    }, 1000);
}

fetchData(42, (error, data) => {
    if (error) {
        console.log("Error:", error);
    } else {
        console.log("Data received:", data);
    }
});

// Array methods use callbacks
const numbers = [1, 2, 3, 4, 5];

// filter uses a callback to test each element
const evens = numbers.filter((n) => n % 2 === 0);
console.log(evens); // [2, 4]

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

Callback Hell

Too many nested callbacks create unreadable code ("callback hell"). Modern solutions like Promises and async/await make asynchronous code much cleaner.

IIFE (Immediately Invoked Function Expression)

An IIFE is a function that runs immediately after it's defined. It's useful for creating private scopes and avoiding global namespace pollution.

JavaScript
// Basic IIFE syntax
(function() {
    console.log("I run immediately!");
})();

// Arrow function IIFE
(() => {
    console.log("Arrow IIFE!");
})();

// IIFE with parameters
(function(name) {
    console.log("Hello,", name);
})("World");

// IIFE returning a value
const result = (function() {
    const privateVar = "secret";
    return privateVar.toUpperCase();
})();

console.log(result); // "SECRET"

// Module pattern using IIFE
const calculator = (function() {
    // Private variables
    let history = [];
    
    // Private function
    function addToHistory(operation) {
        history.push(operation);
    }
    
    // Public interface
    return {
        add(a, b) {
            const result = a + b;
            addToHistory(`${a} + ${b} = ${result}`);
            return result;
        },
        subtract(a, b) {
            const result = a - b;
            addToHistory(`${a} - ${b} = ${result}`);
            return result;
        },
        getHistory() {
            return [...history]; // Return copy
        }
    };
})();

console.log(calculator.add(5, 3));      // 8
console.log(calculator.subtract(10, 4)); // 6
console.log(calculator.getHistory());
// ["5 + 3 = 8", "10 - 4 = 6"]

// Private variables are inaccessible
// console.log(calculator.history); // undefined

Recursion

Recursion is when a function calls itself. It's useful for problems that can be broken down into smaller, similar subproblems.

JavaScript
// Factorial: n! = n × (n-1) × (n-2) × ... × 1
function factorial(n) {
    // Base case
    if (n <= 1) {
        return 1;
    }
    // Recursive case
    return n * factorial(n - 1);
}

console.log(factorial(5)); // 120 (5 × 4 × 3 × 2 × 1)

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

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

// Sum of array using recursion
function sumArray(arr) {
    if (arr.length === 0) return 0;
    return arr[0] + sumArray(arr.slice(1));
}

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

// Flatten nested array
function flatten(arr) {
    let result = [];
    
    for (const item of arr) {
        if (Array.isArray(item)) {
            result = result.concat(flatten(item));
        } else {
            result.push(item);
        }
    }
    
    return result;
}

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

// Deep clone object
function deepClone(obj) {
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }
    
    if (Array.isArray(obj)) {
        return obj.map(item => deepClone(item));
    }
    
    const cloned = {};
    for (const key in obj) {
        cloned[key] = deepClone(obj[key]);
    }
    return cloned;
}

const original = { a: 1, b: { c: 2, d: [3, 4] } };
const clone = deepClone(original);
clone.b.c = 999;
console.log(original.b.c); // 2 (unchanged)

Recursion Tips

  • Always have a base case to stop recursion
  • Make sure each recursive call moves toward the base case
  • Watch for stack overflow with very deep recursion
  • Consider tail call optimization for performance

The 'this' Keyword

The this keyword refers to the object that is executing the current function. Its value depends on how the function is called.

JavaScript
// 'this' in a method - refers to the object
const person = {
    name: "John",
    greet() {
        console.log(`Hello, I'm ${this.name}`);
    }
};

person.greet(); // "Hello, I'm John"

// 'this' in a regular function - undefined (strict) or global
function showThis() {
    console.log(this);
}
showThis(); // undefined (in strict mode) or Window/global

// 'this' problem with callbacks
const timer = {
    seconds: 0,
    start() {
        // Problem: 'this' loses context in callback
        setInterval(function() {
            // this.seconds++; // Error! 'this' is not timer
            console.log("Regular function this:", this);
        }, 1000);
    }
};

// Solution 1: Arrow function (inherits 'this')
const timer2 = {
    seconds: 0,
    start() {
        setInterval(() => {
            this.seconds++; // Works! Arrow function uses outer 'this'
            console.log(this.seconds);
        }, 1000);
    }
};

// Solution 2: Store 'this' in a variable
const timer3 = {
    seconds: 0,
    start() {
        const self = this;
        setInterval(function() {
            self.seconds++;
            console.log(self.seconds);
        }, 1000);
    }
};

// Solution 3: bind()
const timer4 = {
    seconds: 0,
    start() {
        setInterval(function() {
            this.seconds++;
            console.log(this.seconds);
        }.bind(this), 1000);
    }
};

call(), apply(), and bind()

JavaScript
function greet(greeting, punctuation) {
    console.log(`${greeting}, ${this.name}${punctuation}`);
}

const user = { name: "Alice" };

// call() - calls function with given 'this' and arguments
greet.call(user, "Hello", "!"); // "Hello, Alice!"

// apply() - like call() but takes arguments as array
greet.apply(user, ["Hi", "?"]); // "Hi, Alice?"

// bind() - returns new function with bound 'this'
const boundGreet = greet.bind(user);
boundGreet("Hey", "."); // "Hey, Alice."

// bind() with partial application
const sayHelloTo = greet.bind(user, "Hello");
sayHelloTo("!!!"); // "Hello, Alice!!!"

// Practical example: borrowing methods
const numbers = {
    values: [1, 2, 3, 4, 5],
    sum() {
        return this.values.reduce((a, b) => a + b, 0);
    }
};

const moreNumbers = {
    values: [10, 20, 30]
};

// Borrow the sum method
console.log(numbers.sum.call(moreNumbers)); // 60

Currying

Currying is a technique where a function with multiple arguments is transformed into a sequence of functions, each taking a single argument.

JavaScript
// Regular function
function add(a, b, c) {
    return a + b + c;
}
console.log(add(1, 2, 3)); // 6

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

console.log(curriedAdd(1)(2)(3)); // 6

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

// Partial application with currying
const add5 = curriedAdd(5);
const add5and10 = add5(10);
console.log(add5and10(3)); // 18

// Practical example: Logging with levels
const createLogger = (level) => (module) => (message) => {
    console.log(`[${level}] [${module}] ${message}`);
};

const errorLogger = createLogger("ERROR");
const authError = errorLogger("Auth");
const dbError = errorLogger("Database");

authError("Invalid credentials");
// [ERROR] [Auth] Invalid credentials

dbError("Connection failed");
// [ERROR] [Database] Connection failed

// Generic curry function
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));
        };
    };
}

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

const curriedMultiply = curry(multiply);
console.log(curriedMultiply(2)(3)(4));    // 24
console.log(curriedMultiply(2, 3)(4));    // 24
console.log(curriedMultiply(2)(3, 4));    // 24
console.log(curriedMultiply(2, 3, 4));    // 24

Partial Application

Partial application is creating a new function by pre-filling some arguments of an existing function.

JavaScript
// Using bind() for partial application
function greet(greeting, name, punctuation) {
    return `${greeting}, ${name}${punctuation}`;
}

// Pre-fill the greeting
const sayHello = greet.bind(null, "Hello");
console.log(sayHello("Alice", "!")); // "Hello, Alice!"
console.log(sayHello("Bob", "?"));   // "Hello, Bob?"

// Pre-fill greeting and name
const helloAlice = greet.bind(null, "Hello", "Alice");
console.log(helloAlice("!")); // "Hello, Alice!"
console.log(helloAlice(".")); // "Hello, Alice."

// Custom partial function
function partial(fn, ...presetArgs) {
    return function(...laterArgs) {
        return fn(...presetArgs, ...laterArgs);
    };
}

const multiply = (a, b, c) => a * b * c;
const double = partial(multiply, 2);
const doubleThenTriple = partial(multiply, 2, 3);

console.log(double(3, 4));      // 24 (2 * 3 * 4)
console.log(doubleThenTriple(5)); // 30 (2 * 3 * 5)

// Practical example: API request helper
function request(baseURL, method, endpoint, data) {
    console.log(`${method} ${baseURL}${endpoint}`, data);
    // Actual fetch logic would go here
}

// Create specialized request functions
const apiRequest = partial(request, "https://api.example.com");
const getRequest = partial(request, "https://api.example.com", "GET");
const postRequest = partial(request, "https://api.example.com", "POST");

getRequest("/users", {});
// GET https://api.example.com/users {}

postRequest("/users", { name: "John" });
// POST https://api.example.com/users { name: "John" }

Memoization

Memoization is an optimization technique that caches function results based on their arguments. It's perfect for expensive computations that are called repeatedly with the same inputs.

JavaScript
// Basic memoization
function memoize(fn) {
    const cache = new Map();
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (cache.has(key)) {
            console.log("Cache hit for:", key);
            return cache.get(key);
        }
        
        console.log("Computing for:", key);
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

// Expensive fibonacci without memoization
function slowFib(n) {
    if (n <= 1) return n;
    return slowFib(n - 1) + slowFib(n - 2);
}

// Memoized version - dramatically faster!
const fastFib = memoize(function fib(n) {
    if (n <= 1) return n;
    return fastFib(n - 1) + fastFib(n - 2);
});

console.time("slowFib(35)");
console.log(slowFib(35));  // Takes several seconds!
console.timeEnd("slowFib(35)");

console.time("fastFib(35)");
console.log(fastFib(35));  // Nearly instant!
console.timeEnd("fastFib(35)");

// Memoization with WeakMap for object arguments
function memoizeWithWeakMap(fn) {
    const cache = new WeakMap();
    
    return function(obj) {
        if (cache.has(obj)) {
            return cache.get(obj);
        }
        
        const result = fn(obj);
        cache.set(obj, result);
        return result;
    };
}

// Memoize with custom key function
function memoizeWithKeyFn(fn, keyFn) {
    const cache = new Map();
    
    return function(...args) {
        const key = keyFn ? keyFn(...args) : JSON.stringify(args);
        
        if (!cache.has(key)) {
            cache.set(key, fn.apply(this, args));
        }
        
        return cache.get(key);
    };
}

// Example: memoize API calls
const fetchUser = memoizeWithKeyFn(
    async (userId) => {
        const response = await fetch(`/api/users/${userId}`);
        return response.json();
    },
    (userId) => `user-${userId}`
);

// Memoization with cache expiration
function memoizeWithTTL(fn, ttlMs = 60000) {
    const cache = new Map();
    
    return function(...args) {
        const key = JSON.stringify(args);
        const cached = cache.get(key);
        
        if (cached && Date.now() - cached.timestamp < ttlMs) {
            return cached.value;
        }
        
        const result = fn.apply(this, args);
        cache.set(key, { value: result, timestamp: Date.now() });
        return result;
    };
}

When NOT to Memoize

  • Functions with side effects (API calls without caching strategy)
  • Functions that return different results for same inputs (random, Date.now)
  • Functions called with unique arguments each time
  • When memory is more constrained than CPU

Function Composition

Function composition is combining simple functions to build more complex ones. The output of one function becomes the input of the next.

JavaScript
// Simple composition (right to left)
const compose = (...fns) => (x) => 
    fns.reduceRight((acc, fn) => fn(acc), x);

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

// Example functions
const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;

// Using compose (right to left)
const composed = compose(square, double, addOne);
console.log(composed(2));  // 36
// Steps: 2 → addOne → 3 → double → 6 → square → 36

// Using pipe (left to right)
const piped = pipe(addOne, double, square);
console.log(piped(2));     // 36
// Same result, but reads in order of execution

// Real-world example: text processing
const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const replaceSpaces = str => str.replace(/\s+/g, '-');
const removeSpecialChars = str => str.replace(/[^\w-]/g, '');

const slugify = pipe(
    trim,
    toLowerCase,
    replaceSpaces,
    removeSpecialChars
);

console.log(slugify("  Hello World! How are you?  "));
// "hello-world-how-are-you"

// Compose with multiple arguments (point-free style)
const map = fn => arr => arr.map(fn);
const filter = pred => arr => arr.filter(pred);
const reduce = (fn, init) => arr => arr.reduce(fn, init);

const sum = reduce((a, b) => a + b, 0);
const isEven = x => x % 2 === 0;
const doubleNum = x => x * 2;

const sumOfDoubledEvens = pipe(
    filter(isEven),
    map(doubleNum),
    sum
);

console.log(sumOfDoubledEvens([1, 2, 3, 4, 5, 6]));
// [2, 4, 6] → [4, 8, 12] → 24

// Async composition
const pipeAsync = (...fns) => (x) =>
    fns.reduce(async (acc, fn) => fn(await acc), x);

const fetchUser = async id => ({ id, name: "John" });
const fetchPosts = async user => ({ ...user, posts: ["post1", "post2"] });
const formatData = async data => ({ ...data, formatted: true });

const getUserWithPosts = pipeAsync(fetchUser, fetchPosts, formatData);
getUserWithPosts(1).then(console.log);
// { id: 1, name: "John", posts: ["post1", "post2"], formatted: true }

Trampolining

Trampolining is a technique to optimize recursive functions and avoid stack overflow by converting recursion to iteration.

JavaScript
// Problem: Deep recursion causes stack overflow
function factorialBad(n) {
    if (n <= 1) return 1;
    return n * factorialBad(n - 1);
}
// factorialBad(100000); // RangeError: Maximum call stack size exceeded

// Trampoline function
function trampoline(fn) {
    return function(...args) {
        let result = fn(...args);
        
        // Keep calling while result is a function
        while (typeof result === "function") {
            result = result();
        }
        
        return result;
    };
}

// Trampolined factorial
function factorialTrampoline(n, acc = 1) {
    if (n <= 1) return acc;
    // Return a function instead of calling recursively
    return () => factorialTrampoline(n - 1, n * acc);
}

const factorial = trampoline(factorialTrampoline);
console.log(factorial(5));      // 120
console.log(factorial(100));    // 9.33e157
// console.log(factorial(100000)); // Works! No stack overflow

// Trampolined sum
function sumToN(n, acc = 0) {
    if (n <= 0) return acc;
    return () => sumToN(n - 1, acc + n);
}

const sum = trampoline(sumToN);
console.log(sum(10));      // 55
console.log(sum(10000));   // 50005000 (no stack overflow!)

// Mutual recursion with trampoline
function isEven(n) {
    if (n === 0) return true;
    return () => isOdd(n - 1);
}

function isOdd(n) {
    if (n === 0) return false;
    return () => isEven(n - 1);
}

const checkEven = trampoline(isEven);
const checkOdd = trampoline(isOdd);

console.log(checkEven(10000));  // true
console.log(checkOdd(10001));   // true

// Alternative: Convert to iteration manually
function factorialIterative(n) {
    let result = 1;
    for (let i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
}

// When to use trampolining:
// - Deep recursive algorithms
// - Tail-recursive functions
// - Mutual recursion
// - When refactoring to iteration is complex

Generator Functions

Generators are functions that can pause execution and resume later. They're perfect for lazy evaluation, infinite sequences, and implementing iterators.

JavaScript
// Basic generator function (note the *)
function* countUp() {
    yield 1;
    yield 2;
    yield 3;
}

const counter = countUp();
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: 3, done: false }
console.log(counter.next()); // { value: undefined, done: true }

// Generators are iterable
for (const num of countUp()) {
    console.log(num);  // 1, 2, 3
}

console.log([...countUp()]);  // [1, 2, 3]

// Infinite sequence (lazy evaluation)
function* infiniteNumbers(start = 0) {
    let n = start;
    while (true) {
        yield n++;
    }
}

const numbers = infiniteNumbers();
console.log(numbers.next().value);  // 0
console.log(numbers.next().value);  // 1
console.log(numbers.next().value);  // 2
// Doesn't create infinite array - values generated on demand

// Taking N items from infinite generator
function* take(n, iterable) {
    let count = 0;
    for (const item of iterable) {
        if (count >= n) return;
        yield item;
        count++;
    }
}

console.log([...take(5, infiniteNumbers())]);  // [0, 1, 2, 3, 4]

// Passing values INTO generators
function* conversation() {
    const name = yield "What's your name?";
    const age = yield `Hello, ${name}! How old are you?`;
    return `${name} is ${age} years old.`;
}

const chat = conversation();
console.log(chat.next().value);          // "What's your name?"
console.log(chat.next("Alice").value);   // "Hello, Alice! How old are you?"
console.log(chat.next(30).value);        // "Alice is 30 years old."

// Generator for fibonacci sequence
function* fibonacci() {
    let [prev, curr] = [0, 1];
    while (true) {
        yield curr;
        [prev, curr] = [curr, prev + curr];
    }
}

console.log([...take(10, fibonacci())]);
// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

// Composing generators with yield*
function* letters() {
    yield 'a';
    yield 'b';
    yield 'c';
}

function* numbers() {
    yield 1;
    yield 2;
    yield 3;
}

function* combined() {
    yield* letters();  // Delegate to another generator
    yield* numbers();
}

console.log([...combined()]);  // ['a', 'b', 'c', 1, 2, 3]

// Async generators (ES2018)
async function* fetchPages(urls) {
    for (const url of urls) {
        const response = await fetch(url);
        yield await response.json();
    }
}

// Using async generator
async function processPages() {
    const urls = ['/api/page/1', '/api/page/2', '/api/page/3'];
    
    for await (const page of fetchPages(urls)) {
        console.log(page);
    }
}

// Custom iterable with generator
const range = {
    from: 1,
    to: 5,
    
    *[Symbol.iterator]() {
        for (let i = this.from; i <= this.to; i++) {
            yield i;
        }
    }
};

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

Generator Use Cases

  • Lazy sequences: Generate values on demand without memory overhead
  • Infinite data: Model infinite streams like Fibonacci, primes
  • Pagination: Fetch data page by page as needed
  • State machines: Model complex state transitions
  • Cooperative multitasking: Simulate concurrency

Summary

Closures

Functions that remember their outer scope, enabling private state and factories

Higher-Order Functions

Functions that take/return functions for powerful abstractions

Callbacks

Functions passed as arguments, executed later

IIFE

Immediately invoked for private scope

Recursion

Functions calling themselves with base cases

this & bind

Context binding with call, apply, bind