Lesson 4 of 8
Higher-Order Functions
Functions that take or return other functions - map, filter, reduce and beyond
What are Higher-Order Functions?
A Higher-Order Function (HOF) is a function that either takes one or more functions as arguments, returns a function, or both. HOFs are fundamental to functional programming and enable powerful abstractions.
// Takes a function as argument
function doTwice(fn, x) {
return fn(fn(x));
}
doTwice(x => x * 2, 5); // 20
// Returns a function
function multiplier(factor) {
return x => x * factor;
}
const double = multiplier(2);
double(5); // 10
// Both
function compose(f, g) {
return x => f(g(x));
}
const addOneThenDouble = compose(x => x * 2, x => x + 1);
addOneThenDouble(5); // 12
The Big Three: map, filter, reduce
const numbers = [1, 2, 3, 4, 5];
// MAP: Transform each element
const doubled = numbers.map(n => n * 2);
// [2, 4, 6, 8, 10]
// FILTER: Keep elements that pass a test
const evens = numbers.filter(n => n % 2 === 0);
// [2, 4]
// REDUCE: Combine all elements into one value
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 15
// Chaining them together
const result = [1, 2, 3, 4, 5]
.filter(n => n % 2 === 0) // [2, 4]
.map(n => n * 10) // [20, 40]
.reduce((acc, n) => acc + n, 0); // 60
Deep Dive: map
// map applies a function to each element
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 35 },
];
// Extract property
const names = users.map(user => user.name);
// ['Alice', 'Bob', 'Charlie']
// Transform objects
const withBirthYear = users.map(user => ({
...user,
birthYear: 2024 - user.age,
}));
// With index
const indexed = users.map((user, index) => ({
...user,
id: index + 1,
}));
// Implementing map ourselves
function myMap(arr, fn) {
const result = [];
for (let i = 0; i < arr.length; i++) {
result.push(fn(arr[i], i, arr));
}
return result;
}
Deep Dive: filter
const products = [
{ name: 'Laptop', price: 999, inStock: true },
{ name: 'Phone', price: 699, inStock: false },
{ name: 'Tablet', price: 449, inStock: true },
{ name: 'Watch', price: 299, inStock: true },
];
// Filter by condition
const available = products.filter(p => p.inStock);
const expensive = products.filter(p => p.price > 500);
const affordableInStock = products.filter(p => p.inStock && p.price < 500);
// Remove falsy values
const mixed = [0, 'hello', '', null, 42, undefined, 'world'];
const truthy = mixed.filter(Boolean);
// ['hello', 42, 'world']
// Remove duplicates (with Set is better, but filter can do it)
const nums = [1, 2, 2, 3, 3, 3, 4];
const unique = nums.filter((n, i, arr) => arr.indexOf(n) === i);
// [1, 2, 3, 4]
// Implementing filter ourselves
function myFilter(arr, predicate) {
const result = [];
for (let i = 0; i < arr.length; i++) {
if (predicate(arr[i], i, arr)) {
result.push(arr[i]);
}
}
return result;
}
Deep Dive: reduce
// reduce is the most powerful HOF - can implement map and filter!
const numbers = [1, 2, 3, 4, 5];
// Sum
const sum = numbers.reduce((acc, n) => acc + n, 0); // 15
// Product
const product = numbers.reduce((acc, n) => acc * n, 1); // 120
// Find max
const max = numbers.reduce((a, b) => a > b ? a : b);
// Count occurrences
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const counts = fruits.reduce((acc, fruit) => ({
...acc,
[fruit]: (acc[fruit] || 0) + 1,
}), {});
// { apple: 3, banana: 2, orange: 1 }
// Group by property
const people = [
{ name: 'Alice', city: 'NYC' },
{ name: 'Bob', city: 'LA' },
{ name: 'Charlie', city: 'NYC' },
];
const byCity = people.reduce((acc, person) => ({
...acc,
[person.city]: [...(acc[person.city] || []), person],
}), {});
// { NYC: [{Alice}, {Charlie}], LA: [{Bob}] }
// Flatten array
const nested = [[1, 2], [3, 4], [5]];
const flat = nested.reduce((acc, arr) => [...acc, ...arr], []);
// [1, 2, 3, 4, 5]
// Implement map with reduce
const mapWithReduce = (arr, fn) =>
arr.reduce((acc, x, i) => [...acc, fn(x, i)], []);
// Implement filter with reduce
const filterWithReduce = (arr, pred) =>
arr.reduce((acc, x, i) => pred(x, i) ? [...acc, x] : acc, []);
Other Useful HOFs
const numbers = [1, 2, 3, 4, 5];
// find - returns first match
const firstEven = numbers.find(n => n % 2 === 0); // 2
// findIndex - returns index of first match
const firstEvenIndex = numbers.findIndex(n => n % 2 === 0); // 1
// some - returns true if any element passes
const hasEven = numbers.some(n => n % 2 === 0); // true
// every - returns true if all elements pass
const allPositive = numbers.every(n => n > 0); // true
// flatMap - map + flatten
const sentences = ['Hello world', 'Foo bar'];
const words = sentences.flatMap(s => s.split(' '));
// ['Hello', 'world', 'Foo', 'bar']
// forEach - side effects (not pure FP, but useful)
numbers.forEach(n => console.log(n));
Creating Your Own HOFs
// Function factory
function createValidator(rules) {
return value => rules.every(rule => rule(value));
}
const isValidPassword = createValidator([
pwd => pwd.length >= 8,
pwd => /[A-Z]/.test(pwd),
pwd => /[0-9]/.test(pwd),
]);
isValidPassword('Abc12345'); // true
isValidPassword('weak'); // false
// Debounce - classic HOF
function debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
const debouncedSearch = debounce(query => {
console.log('Searching:', query);
}, 300);
// Memoize - cache function results
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
const expensiveCalc = memoize(n => {
console.log('Computing...');
return n * 100;
});
expensiveCalc(5); // 'Computing...' → 500
expensiveCalc(5); // → 500 (cached, no log)
// Once - run only once
function once(fn) {
let called = false;
let result;
return (...args) => {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}
const initialize = once(() => console.log('Initialized!'));
initialize(); // 'Initialized!'
initialize(); // (nothing)