---
title: 'Higher-Order Functions'
source: 'https://academia.sh/en/courses/javascript-object-model/higher-order-functions'
course: 'Objects and Functions in JavaScript'
language: en
updated: '2026-08-23T07:00:59+00:00'
license: 'CC BY-SA 4.0'
---

# Higher-Order Functions

Using a function as a value, the filter-map-reduce chain, functions that produce a comparator, composition, and behavior-adding wrappers.

Earlier lessons constantly used functions as values: passed as a callback, returned
from a factory, written as a property onto an object, a new one produced with `bind`.
All of this rests on a single fact — in JavaScript a function is an object; aside from
being callable, it is no different from a number or a string.

A function that takes a function as an argument or returns a function is called a
**higher-order function**. The Programming Fundamentals course's Introduction to
Functional Programming lesson introduced the concept. This lesson applies it to
measurement records and shows where the previous lessons' binding rules touch this
usage.

## Giving a Function as an Argument

The most common form is an operation on a collection taking **what to do** from
outside. The filter criterion, the transformation rule, and the accumulator operation
can each be held in separately named functions.

```js
const records = [
  { sensor: "S-01", value: 21.4, time: 1000 },
  { sensor: "S-02", value: 19.8, time: 1060 },
  { sensor: "S-01", value: 25.1, time: 1120 },
  { sensor: "S-03", value: 18.2, time: 1180 },
  { sensor: "S-01", value: 22.0, time: 1240 },
];

const aboveThreshold = (record) => record.value > 20;
const getValue = (record) => record.value;
const sum = (total, value) => total + value;

const selected = records.filter(aboveThreshold);
const values = selected.map(getValue);
const total = values.reduce(sum, 0);

console.log(selected.length);
console.log(values.join(","));
console.log(total.toFixed(1));
console.log((total / values.length).toFixed(3));

const groupedBySensor = records.reduce((group, record) => {
  (group[record.sensor] ??= []).push(record.value);
  return group;
}, Object.create(null));

console.log(Object.keys(groupedBySensor).join(","));
console.log(groupedBySensor["S-01"].join(","));
console.log(typeof aboveThreshold);
console.log(aboveThreshold.length);
```

```
3
21.4,25.1,22
68.5
22.833
S-01,S-02,S-03
21.4,25.1,22
function
1
```

Filter, map, and reduce separate three distinct responsibilities: which records are of
interest, which piece of information is taken from those records, and how that
information gets combined. Each can be tested separately and reused in other contexts.

The grouping example shows the generality of reduce: the accumulator does not have to
be a number, it can also be an object. Building the accumulator with
`Object.create(null)` is deliberate — because sensor names come from the data source,
this removes any risk of colliding with names coming from the chain.

The last two lines confirm the function is an ordinary value: its type can be queried,
its parameter count read, and it can be stored in a variable.

## Functions That Return a Function

The second form is a function that produces a function. The produced function carries
the producer's parameters through a closure, giving a specialized function whose
behavior is configured from outside.

```js
function byKey(keyFn, reverse = false) {
  const direction = reverse ? -1 : 1;
  return (a, b) => {
    const leftKey = keyFn(a);
    const rightKey = keyFn(b);
    if (leftKey < rightKey) return -1 * direction;
    if (leftKey > rightKey) return 1 * direction;
    return 0;
  };
}

const records = [
  { sensor: "S-02", value: 19.8 },
  { sensor: "S-01", value: 25.1 },
  { sensor: "S-03", value: 18.2 },
  { sensor: "S-01", value: 21.4 },
];

const format = (list) => list.map((k) => `${k.sensor}:${k.value}`).join(" ");

console.log(format([...records].sort(byKey((k) => k.value))));
console.log(format([...records].sort(byKey((k) => k.value, true))));
console.log(format([...records].sort(byKey((k) => k.sensor))));
console.log(format(records));
```

```
S-03:18.2 S-02:19.8 S-01:21.4 S-01:25.1
S-01:25.1 S-01:21.4 S-02:19.8 S-03:18.2
S-01:25.1 S-01:21.4 S-02:19.8 S-03:18.2
S-02:19.8 S-01:25.1 S-03:18.2 S-01:21.4
```

A single `byKey` function produces three separate sort criteria. The comparison logic
is written once; the only thing that changes is how the key is extracted. This is the
function-level counterpart of the compound-key idea from the Algorithms course's
Choosing a Sorting Algorithm lesson.

The third line shows the sort's **stability**: sorted by sensor name, the two `S-01`
records keep their relative order from the input. The last line confirms the source is
unchanged — because sort works in place, a copy was taken with spread on every call.

## Composition

The second use of returning a function is **composing** several transformations into a
single function. Each step is written on its own; the order is set up once.

```js
function createCalibrator(offset, multiplier) {
  return (rawValue) => (rawValue + offset) * multiplier;
}

function compose(...fns) {
  return (start) => fns.reduce((value, fn) => fn(value), start);
}

const sensorS01 = createCalibrator(-0.4, 1.02);
const sensorS02 = createCalibrator(0.15, 1.0);

console.log(sensorS01(21.4).toFixed(3));
console.log(sensorS02(19.8).toFixed(3));

const round = (value) => Math.round(value * 10) / 10;
const celsiusToFahrenheit = (value) => value * 1.8 + 32;

const pipeline = compose(sensorS01, round, celsiusToFahrenheit, round);

console.log(pipeline(21.4));
console.log(pipeline(19.8));
console.log(compose()(5));
```

```
21.420
19.950
70.5
67.6
5
```

`compose` is itself a reduce: the starting value is the input, the accumulator
operation is "apply the next function." Called with no arguments, it produces the
identity function — no transformation applied — and this is the sign that the
composition is correctly defined.

`createCalibrator` and partial application done with `bind` give the same result. The
difference is in readability and context: the closure form uses no `this`, which
parameter was fixed is clear from its name, and it is subject to none of the binding
rules.

## Passing a Method as a Value

When a **method** is given to a higher-order function, the previous lessons'
detachment problem returns: what is passed is the function, not the object it sits on.

```js
class MeasurementRecord {
  constructor(sensor, value) {
    this.sensor = sensor;
    this.value = value;
  }
  label() {
    return `${this.sensor}:${this.value}`;
  }
}

const records = [new MeasurementRecord("S-01", 21.4), new MeasurementRecord("S-02", 19.8)];

try {
  console.log(records.map(MeasurementRecord.prototype.label).join(" "));
} catch (error) {
  console.log(`direct method: ${error.constructor.name}`);
}

console.log(records.map((record) => record.label()).join(" "));
console.log(records.map(Function.prototype.call, MeasurementRecord.prototype.label).join(" "));

function applyToAll(list, fn, context) {
  const result = [];
  for (const item of list) result.push(fn.call(context, item));
  return result;
}

console.log(applyToAll([1, 2, 3], function (n) {
  return n * this.multiplier;
}, { multiplier: 10 }).join(","));
```

```
direct method: TypeError
S-01:21.4 S-02:19.8
S-01:21.4 S-02:19.8
10,20,30
```

When the prototype's method is given directly, `this` is not bound. The most readable
fix is to wrap the call in an arrow function. The third line is a more indirect path
using `map`'s context parameter: the mapped function becomes `call`, and the context
becomes the actual method; every element is passed as `call`'s first argument. Short to
write, hard to read — a good example of trading readability for brevity.

The last section shows how to offer context support in your own higher-order
functions: the form `fn.call(context, item)` establishes the same contract as built-in
array methods' second parameter.

## Behavior-Adding Wrappers

The third form takes a function and returns a function that offers the same interface
but with extended behavior. The memoization wrapper in the Closures lesson was an
example of this; the pattern is general.

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

function wrapValidated(fn, isValid, message) {
  return function (...args) {
    if (!args.every(isValid)) throw new RangeError(message);
    return fn.apply(this, args);
  };
}

let setupCount = 0;
const setup = once((sensor) => {
  setupCount += 1;
  return `${sensor} set up`;
});

console.log(setup("S-01"));
console.log(setup("S-02"));
console.log(setupCount);

const safeAverage = wrapValidated(
  (...values) => values.reduce((t, d) => t + d, 0) / values.length,
  (value) => typeof value === "number" && Number.isFinite(value),
  "all values must be finite numbers",
);

console.log(safeAverage(21.4, 19.8, 25.1).toFixed(3));
try {
  safeAverage(21.4, "twenty");
} catch (error) {
  console.log(`${error.constructor.name}: ${error.message}`);
}

const record = {
  sensor: "S-01",
  summary: wrapValidated(
    function (prefix) {
      return `${prefix}-${this.sensor}`;
    },
    (a) => typeof a === "string",
    "prefix must be a string",
  ),
};
console.log(record.summary("measurement"));
```

```
S-01 set up
S-01 set up
1
22.100
RangeError: all values must be finite numbers
measurement-S-01
```

Both wrappers follow three rules. They accept the same parameters — this is why rest
parameters are used. They give the same return value. And they preserve context: their
bodies are function expressions, not arrow functions, and `this` is passed along in the
`apply` call. The last section is a consequence of this third rule — when the wrapped
function is called as an object's method, `this` binds to the correct object.

Had the wrapper been written as an arrow function, `this` would have come from the
scope the wrapper was defined in, and it could not have been used as a method. The
boundary on arrow functions applies here too.

## Summary

- A function is a value: it is stored in a variable, given as an argument, returned,
  and carries properties.
- Filter, map, and reduce separate responsibilities by taking a collection operation's
  criterion from outside; reduce's accumulator can also be an object.
- Functions that return a function produce specialized functions that carry their
  parameters through a closure; sort criteria and calibrators are typical examples.
- Composition is a reduce that applies transformations, each written on its own, in
  sequence.
- Giving a method directly to a higher-order function breaks its bond; an arrow
  function wrapping the call is the most readable fix.
- Wrappers must preserve the same parameters and return value and pass context along
  with `apply`; this is why they cannot be written as arrow functions.

## Next Step

In this lesson, grouping was done with the accumulator built as `Object.create(null)`;
the reason was that sensor names could collide with names coming from the chain. The
same precaution was needed in this course's first lesson too, and in the closures'
cache. Using an object as a dictionary has this and other limits: keys are converted to
strings, order guarantees are restricted, and element count cannot be read directly.
The next topic opens by introducing the types the language sets aside for this job —
keyed and single-value collections.
