JavaScript · Functions

JavaScript Higher-Order Functions — Currying, Composition, Memoization and More

6 min read Updated 2026-06-18

Practice Higher-Order Functions interview questions

Functions as values

JavaScript treats functions as first-class values: you can store them in variables, pass them as arguments, and return them from other functions. A higher-order function (HOF) is any function that does the latter two — it takes a function as an argument, returns a function as a result, or both. This single capability underpins a huge amount of idiomatic JavaScript: array methods, event handling, functional composition, and patterns like memoization and debouncing. Understanding HOFs deeply turns you from someone who uses map into someone who can build the next map.

Callbacks: passing functions in

The simplest HOFs accept a function to call — a callback. The array iteration methods are the canonical example.

[1, 2, 3].map(n => n * 2)              // map takes a function
[1, 2, 3].filter(n => n % 2 === 1)     // so does filter
button.addEventListener('click', handleClick)   // and event APIs

Passing behavior as data is what makes these methods reusable: map doesn't know or care what transformation you want — you supply it. A common pitfall is passing a method reference and losing this, or the famous ['1','2','3'].map(parseInt) trap (where parseInt receives the index as its radix argument). Wrap in an explicit arrow when in doubt: .map(s => parseInt(s, 10)).

Returning functions

HOFs that return functions let you build specialized functions on the fly. A function factory captures configuration in a closure and returns a tailored function.

function multiplier(factor) {
  return (n) => n * factor          // returned function closes over `factor`
}

const double = multiplier(2)
const triple = multiplier(3)
double(5)   // 10
triple(5)   // 15

double and triple are distinct functions, each remembering its own factor. This is the foundation of currying and partial application.

Function composition

Composition combines small functions into a pipeline where each function's output feeds the next. It's the functional-programming way to build complex transformations from simple, testable pieces.

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

const clean = pipe(
  s => s.trim(),
  s => s.toLowerCase(),
  s => s.replace(/\s+/g, '-')
)
clean('  Hello World  ')   // 'hello-world'

pipe reads left-to-right (the order data flows); compose reads right-to-left (the mathematical convention). Both are tiny HOFs built on reduce.

Currying

Currying transforms a multi-argument function into a chain of single-argument functions. It enables maximum reuse and partial specialization.

const add = (a) => (b) => (c) => a + b + c
add(1)(2)(3)   // 6

const add10 = add(10)       // partially applied
add10(5)(2)    // 17

Each call returns a function awaiting the next argument until all are supplied. Curried functions compose beautifully because they're naturally unary (one argument), fitting straight into pipe/compose.

Partial application

Partial application fixes some of a function's arguments, producing a new function that takes the rest. It's subtly different from currying (which fixes one at a time) but serves a similar goal. Function.prototype.bind does partial application built-in.

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

const sayHello = greet.bind(null, 'Hello')   // fix the greeting
sayHello('Ada')   // 'Hello, Ada!'

You can also write a generic partial HOF that captures leading arguments and forwards the rest — useful for adapting general functions to specific call sites.

Memoization

Memoization is a HOF that wraps a function to cache its results by arguments, so repeated calls with the same input return instantly. Great for pure, expensive computations.

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

const slowSquare = (n) => { /* expensive */ return n * n }
const fastSquare = memoize(slowSquare)

The cache lives in the returned closure, persisting across calls. Memoization only works for pure functions (same input -> same output) and trades memory for speed — be mindful of unbounded cache growth.

Debounce and throttle

Two HOFs critical for performance with frequent events (scroll, resize, input): debounce delays a function until activity stops, and throttle caps how often it can run. Both return a wrapped function that manages timers in a closure.

function debounce(fn, delay) {
  let timer
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)   // reset on each call
  }
}

const onSearch = debounce(query => fetchResults(query), 300)

Debounce is ideal for search-as-you-type (wait until the user pauses); throttle suits scroll/resize handlers (run at most every N ms). Both prevent expensive work from firing on every single event.

once: run at most one time

A small but illustrative HOF wraps a function so it runs only the first time, caching its result thereafter — handy for one-time initialization.

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

This pattern uses the same closure-over-state technique as memoize and debounce — a recurring theme: HOFs that return stateful wrappers.

Why HOFs matter

Higher-order functions let you treat behavior as data, eliminating duplication and enabling declarative code. Instead of copy-pasting loops with slightly different bodies, you write the loop once (map, filter) and vary the function. Instead of repeating timing logic, you write debounce once. This is the essence of abstraction in functional JavaScript — and why the technique appears everywhere from React hooks to middleware to Array methods.

Key takeaways

  • A higher-order function takes a function as an argument, returns one, or both — enabled by first-class functions.
  • Callbacks (passing functions in) power array methods and event APIs; watch the map(parseInt) and lost-this traps.
  • Returning functions creates factories, currying, and partial application for reusable, specialized functions.
  • compose/pipe build pipelines from small functions using reduce.
  • Memoize, debounce, throttle, and once are HOFs that return stateful wrappers using closures.
  • HOFs let you treat behavior as data — the foundation of declarative, DRY JavaScript.

Once you see functions as values you can pass and return, a whole expressive, reusable style of programming opens up — and you understand the machinery behind the tools you use every day.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.