Advanced function techniques enable reusable, composable, and performant code. This chapter builds on scope and closures from the intermediate level.

Higher-Order Functions

Functions that take other functions as arguments or return functions:

  function repeat(n, action) {
    for (let i = 0; i < n; i++) {
        action(i);
    }
}

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

function greaterThan(n) {
    return x => x > n;
}

[1, 5, 10].filter(greaterThan(4)); // [5, 10]

// Built-in HOFs: map, filter, reduce, forEach, sort, find
[1, 2, 3].map(x => x * 2); // [2, 4, 6]
  

Higher-order functions are the foundation of functional programming in JavaScript.

Currying

Transform a multi-argument function into a chain of single-argument functions:

  function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        }
        return (...more) => curried(...args, ...more);
    };
}

const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3));   // 6
console.log(add(1, 2)(3));   // 6
console.log(add(1, 2, 3));   // 6
  

Useful for partial configuration: const addTax = curry((rate, price) => price * (1 + rate)).

Partial Application

Fix some arguments upfront:

  function partial(fn, ...fixedArgs) {
    return (...remainingArgs) => fn(...fixedArgs, ...remainingArgs);
}

function greet(greeting, name) {
    return `${greeting}, ${name}!`;
}

const sayHello = partial(greet, 'Hello');
console.log(sayHello('Alice')); // 'Hello, Alice!'

// Native: bind
const sayHi = greet.bind(null, 'Hi');
  

Partial application vs currying: partial fixes N arguments at once; currying transforms one argument at a time.

Debounce

Delay execution until after a pause in calls — ideal for search input:

  function debounce(fn, delay) {
    let timeoutId;
    return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => fn.apply(this, args), delay);
    };
}

const search = debounce((query) => {
    fetch(`/api/search?q=${encodeURIComponent(query)}`)
        .then(r => r.json())
        .then(renderResults);
}, 300);

input.addEventListener('input', (e) => search(e.target.value));
  

Only fires 300ms after user stops typing — reduces API calls dramatically.

Debounce with Immediate Option

  function debounce(fn, delay, immediate = false) {
    let timeoutId;
    return function(...args) {
        const callNow = immediate && !timeoutId;
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
            timeoutId = null;
            if (!immediate) fn.apply(this, args);
        }, delay);
        if (callNow) fn.apply(this, args);
    };
}
  

Throttle

Limit execution to at most once per interval — ideal for scroll/resize:

  function throttle(fn, limit) {
    let inThrottle = false;
    return function(...args) {
        if (!inThrottle) {
            fn.apply(this, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

window.addEventListener('scroll', throttle(() => {
    updateScrollIndicator();
}, 200), { passive: true });
  
Pattern Use when
Debounce Wait for pause (search, resize end)
Throttle Regular sampling (scroll, mousemove)

Function Composition

Combine functions into a pipeline:

  const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);

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

console.log(compose(square, double, addOne)(3)); // ((3+1)*2)^2 = 64
console.log(pipe(addOne, double, square)(3));     // same: 64
  

Real-world example:

  const processUser = pipe(
    trimStrings,
    normalizeEmail,
    validateSchema,
    saveToDatabase
);
  

Memoization

Cache function results for repeated inputs:

  function memoize(fn) {
    const cache = new Map();
    return function(...args) {
        const key = JSON.stringify(args);
        if (cache.has(key)) return cache.get(key);
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

const fib = memoize(function fib(n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
});

console.log(fib(40)); // fast due to caching
  

Limit cache size in production to prevent memory growth.

Once

Execute function only once — useful for initialization:

  function once(fn) {
    let called = false;
    let result;
    return function(...args) {
        if (!called) {
            called = true;
            result = fn.apply(this, args);
        }
        return result;
    };
}

const init = once(() => {
    console.log('Initialized once');
    return { config: true };
});
  

Best Practices

  1. Pure functions when possible — same input, same output, no side effects
  2. Name returned functions for clearer stack traces: return function curried(...args)
  3. Document arity for curried functions
  4. Use lodash/debounce in production if battle-tested behavior needed
  5. Test edge cases — rapid calls, this binding, argument changes

Troubleshooting

Debounce never fires

  • Delay too long; function recreated each render (React) — wrap in useCallback/useMemo

Throttle misses final call

  • Combine throttle with debounced trailing call for scroll-end detection

this lost in HOF

  • Use fn.apply(this, args) or arrow functions carefully

Memoize stale results

  • Cache invalidation needed when dependencies change

These patterns appear throughout production JavaScript — in frameworks, utilities, and performance-critical code paths.