Higher-Order Functions
Higher-order functions are functions that operate on other functions, either by taking them as arguments or by returning them. This powerful concept is fundamental to functional programming and makes your code more reusable, composable, and maintainable.
Functions as First-Class Citizens
In JavaScript, functions are first-class citizens, meaning they can be treated like any other value:
// Functions can be assigned to variables
const greet = function(name) {
return `Hello, ${name}!`;
};
// Functions can be stored in arrays
const operations = [
x => x + 1,
x => x * 2,
x => x ** 2
];
console.log(operations[0](5)); // 6
console.log(operations[1](5)); // 10
console.log(operations[2](5)); // 25
// Functions can be stored in objects
const math = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b
};
console.log(math.add(3, 4)); // 7
// Functions can be passed as arguments
function applyOperation(x, operation) {
return operation(x);
}
console.log(applyOperation(5, x => x * 2)); // 10
Tip: Treating functions as values opens up powerful programming patterns. You can build flexible, reusable code by passing behavior (functions) as data.
Functions Returning Functions
Higher-order functions can return new functions, creating powerful abstractions:
// Basic function factory
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const quadruple = createMultiplier(4);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(quadruple(5)); // 20
// With arrow functions (more concise)
const createAdder = x => y => x + y;
const add5 = createAdder(5);
const add10 = createAdder(10);
console.log(add5(3)); // 8
console.log(add10(3)); // 13
Closures and Higher-Order Functions
Returned functions have access to the outer function's variables (closure):
// Counter with private state
function createCounter(initial = 0) {
let count = initial; // Private variable
return {
increment() {
return ++count;
},
decrement() {
return --count;
},
getCount() {
return count;
},
reset() {
count = initial;
return count;
}
};
}
const counter = createCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.getCount()); // 11
console.log(counter.reset()); // 10
// Note: count is not directly accessible
console.log(counter.count); // undefined
Key Concept: Closures allow returned functions to "remember" the environment in which they were created, enabling private state and data encapsulation.
Function Composition
Combining multiple functions to create new functionality:
// Basic composition - right to left
function compose(...functions) {
return function(initialValue) {
return functions.reduceRight(
(value, fn) => fn(value),
initialValue
);
};
}
const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;
// Compose: square(double(addOne(x)))
const compute = compose(square, double, addOne);
console.log(compute(2)); // ((2 + 1) * 2)^2 = 36
// Pipe - left to right (more intuitive reading)
function pipe(...functions) {
return function(initialValue) {
return functions.reduce(
(value, fn) => fn(value),
initialValue
);
};
}
// Pipe: square(double(addOne(x)))
const process = pipe(addOne, double, square);
console.log(process(2)); // ((2 + 1) * 2)^2 = 36
Currying and Partial Application
Currying transforms a function with multiple arguments into a sequence of functions with single arguments:
// Manual currying
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
console.log(curriedAdd(1)(2)(3)); // 6
// Arrow function currying (more concise)
const add = a => b => c => a + b + c;
console.log(add(1)(2)(3)); // 6
// 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, ...moreArgs]);
};
};
}
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
// Partial application
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 triple = partial(multiply, 3);
console.log(double(5, 2)); // 2 * 5 * 2 = 20
console.log(triple(5, 2)); // 3 * 5 * 2 = 30
Currying vs Partial Application: Currying always returns a function that takes exactly one argument at a time. Partial application can take multiple arguments and returns a function that expects the remaining arguments.
Memoization Patterns
Caching function results for performance optimization:
// Basic memoization
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Returning cached result');
return cache.get(key);
}
console.log('Computing result');
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Expensive fibonacci calculation
const fibonacci = memoize(function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
});
console.log(fibonacci(10)); // Computing result (multiple times)
console.log(fibonacci(10)); // Returning cached result
console.log(fibonacci(20)); // Much faster due to memoization
// Memoization with custom key generator
function memoizeWith(fn, keyGenerator) {
const cache = new Map();
return function(...args) {
const key = keyGenerator(...args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const fetchUser = memoizeWith(
(id) => ({ id, name: `User ${id}`, fetched: Date.now() }),
(id) => `user_${id}` // Custom key
);
Practical Higher-Order Function Examples
// 1. Debouncing - delay execution until no more calls
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Usage: Search as user types
const search = debounce((query) => {
console.log(`Searching for: ${query}`);
}, 500);
search('j'); // Won't execute
search('ja'); // Won't execute
search('jav'); // Won't execute
search('java'); // Executes after 500ms
// 2. Throttling - limit execution frequency
function throttle(fn, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Usage: Scroll event handling
const handleScroll = throttle(() => {
console.log('Scroll event handled');
}, 1000);
// 3. Once - function that can only be called once
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
const initialize = once(() => {
console.log('Initializing...');
return { initialized: true };
});
console.log(initialize()); // Logs "Initializing..." and returns object
console.log(initialize()); // Returns cached object without logging
// 4. Retry logic
function retry(fn, maxAttempts = 3, delay = 1000) {
return async function(...args) {
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn.apply(this, args);
} catch (error) {
lastError = error;
console.log(`Attempt ${attempt} failed:`, error.message);
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw new Error(`Failed after ${maxAttempts} attempts: ${lastError.message}`);
};
}
// Usage
const unreliableAPI = retry(async () => {
if (Math.random() > 0.7) {
return 'Success!';
}
throw new Error('Network error');
}, 5, 500);
Array Higher-Order Functions
// Built-in higher-order array methods
const numbers = [1, 2, 3, 4, 5];
// map - transform each element
const doubled = numbers.map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// filter - select elements
const evens = numbers.filter(x => x % 2 === 0);
console.log(evens); // [2, 4]
// reduce - accumulate values
const sum = numbers.reduce((acc, x) => acc + x, 0);
console.log(sum); // 15
// Chaining multiple operations
const result = numbers
.filter(x => x % 2 !== 0) // Odd numbers: [1, 3, 5]
.map(x => x ** 2) // Square: [1, 9, 25]
.reduce((acc, x) => acc + x, 0); // Sum: 35
console.log(result); // 35
// Custom array higher-order function
function mapObject(obj, fn) {
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [key, fn(value, key)])
);
}
const prices = { apple: 1, banana: 2, orange: 3 };
const discounted = mapObject(prices, price => price * 0.8);
console.log(discounted); // { apple: 0.8, banana: 1.6, orange: 2.4 }
Real-World Use Cases
// 1. Middleware pattern (Express.js style)
function createMiddleware() {
const middlewares = [];
return {
use(fn) {
middlewares.push(fn);
},
execute(context) {
return middlewares.reduce(
(promise, middleware) => promise.then(() => middleware(context)),
Promise.resolve()
);
}
};
}
const app = createMiddleware();
app.use(ctx => { ctx.step1 = true; console.log('Middleware 1'); });
app.use(ctx => { ctx.step2 = true; console.log('Middleware 2'); });
// 2. Plugin system
function createPluginSystem() {
const plugins = [];
return {
register(plugin) {
plugins.push(plugin);
},
applyPlugins(data) {
return plugins.reduce(
(result, plugin) => plugin(result),
data
);
}
};
}
const system = createPluginSystem();
system.register(data => ({ ...data, processed: true }));
system.register(data => ({ ...data, timestamp: Date.now() }));
const result = system.applyPlugins({ value: 42 });
console.log(result); // { value: 42, processed: true, timestamp: ... }
// 3. Validation chain
function createValidator() {
const rules = [];
return {
addRule(fn, errorMessage) {
rules.push({ fn, errorMessage });
return this; // Chainable
},
validate(value) {
const errors = [];
for (const { fn, errorMessage } of rules) {
if (!fn(value)) {
errors.push(errorMessage);
}
}
return errors.length === 0 ? null : errors;
}
};
}
const emailValidator = createValidator()
.addRule(v => v && v.length > 0, 'Email is required')
.addRule(v => v.includes('@'), 'Email must contain @')
.addRule(v => v.length < 100, 'Email too long');
console.log(emailValidator.validate('test@example.com')); // null (valid)
console.log(emailValidator.validate('invalid')); // ['Email must contain @']
Performance Note: While higher-order functions are powerful and elegant, be mindful of performance in hot code paths. Sometimes a simple loop is faster than multiple chained operations.
Practice Exercise:
Task: Create utility higher-order functions:
- Write a
logger function that wraps another function and logs its inputs/outputs
- Create a
cache function with TTL (time-to-live) support
- Build a
compose function that works with async functions
Solution:
// 1. Logger wrapper
function logger(fn, name = fn.name || 'anonymous') {
return function(...args) {
console.log(`[${name}] Called with:`, args);
const result = fn.apply(this, args);
console.log(`[${name}] Returned:`, result);
return result;
};
}
const add = logger((a, b) => a + b, 'add');
add(2, 3); // Logs inputs and output
// 2. Cache with TTL
function cache(fn, ttl = 60000) {
const store = new Map();
return function(...args) {
const key = JSON.stringify(args);
const cached = store.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.value;
}
const value = fn.apply(this, args);
store.set(key, { value, timestamp: Date.now() });
// Cleanup expired entries
setTimeout(() => store.delete(key), ttl);
return value;
};
}
const fetchData = cache((id) => {
console.log(`Fetching data for ${id}`);
return { id, data: '....' };
}, 5000);
// 3. Async compose
function composeAsync(...functions) {
return async function(initialValue) {
let result = initialValue;
for (const fn of functions.reverse()) {
result = await fn(result);
}
return result;
};
}
const asyncAdd = async (x) => x + 1;
const asyncDouble = async (x) => x * 2;
const asyncSquare = async (x) => x ** 2;
const processAsync = composeAsync(asyncSquare, asyncDouble, asyncAdd);
processAsync(2).then(console.log); // ((2 + 1) * 2)^2 = 36
Summary
In this lesson, you learned:
- Functions are first-class citizens in JavaScript
- Higher-order functions take functions as arguments or return functions
- Closures enable private state and data encapsulation
- Function composition combines multiple functions into pipelines
- Currying and partial application create specialized functions
- Memoization caches results for performance optimization
- Practical patterns: debounce, throttle, once, retry
- Higher-order functions enable elegant, reusable code patterns
Next Up: In the next lesson, we'll take a deep dive into Closures and explore advanced closure patterns and use cases!