Higher Order Functions in JavaScript – Beginner‘s Guide

Higher order functions are one of the most powerful concepts in JavaScript. Once you understand how to use them, they will completely change the way you write code for the better.

In simple terms, a higher order function is a function that either takes other functions as arguments or returns a function as its result. This is only possible because in JavaScript, functions are first-class citizens – they can be treated just like any other value such as strings or numbers. That means you can assign functions to variables, store them in arrays or objects, pass them to other functions as arguments, or return them from functions.

Higher order functions allow you to write abstract, reusable code that is easy to reason about. Rather than constantly repeating yourself, you can create utility functions that encapsulate common behavior and make your code more expressive and modular. You‘re essentially "teaching" the language new tricks by extending what‘s possible with your own custom functions.

If this sounds confusing, don‘t worry. It will all become clear with some practical examples. Let‘s dive right in and take a look at the two main ways higher order functions work – taking functions as arguments and returning functions as results.

Passing Functions as Arguments

The most common use of higher order functions is passing callback functions as arguments to other functions. A callback function is simply a function that is passed to another function, with the expectation that the callback will be called at the appropriate time.

Callbacks are frequently used in asynchronous programming, where one function needs to wait for another function to complete before continuing. However, they have many other uses as well.

The classic example is the forEach() array method, which takes a callback function as its argument:

const numbers = [1, 2, 3, 4, 5];

numbers.forEach(function(number) {
console.log(number);
});

This loops through the numbers array and calls the callback function for each element, passing in the current element as an argument. The result is that each number gets printed to the console:

1
2
3
4
5

We can make this code more concise by using an arrow function as the callback:

numbers.forEach(number => console.log(number));

Arrow functions provide a compact syntax for writing function expressions. If the function body consists of a single expression, you can even omit the curly braces and return keyword, like we did here.

Let‘s look at a few more examples of passing functions to other functions. Many array methods besides forEach() accept callback functions, including:

  • map() – creates a new array with the results of calling the callback on every element
  • filter() – creates a new array with all elements that pass the test implemented by the callback
  • reduce() – applies the callback to an accumulator and each element to reduce the array to a single value

Here‘s how you could use all three:

const numbers = [1, 2, 3, 4, 5];

// double each number
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// get even numbers only
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4]

// sum all numbers
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue);
console.log(sum); // 15

We can implement our own version of forEach() to demonstrate how this works under the hood:

function forEach(array, callback) {
for (let i = 0; i < array.length; i++) {
callback(array[i]);
}
}

forEach(numbers, number => console.log(number));

Our forEach() function takes an array and a callback function as arguments. It loops through the array and calls the callback for each element, passing in the current element as an argument. This replicates the behavior of the built-in forEach() method.

The ability to pass functions as arguments is an extremely useful tool. It allows you to write flexible, reusable code by deferring some decisions to the function caller. The caller gets to "customize" the behavior of your function by passing in their own callback to be executed at the right time.

Returning Functions From Functions

The other common use of higher order functions is returning functions as the result of other functions. This is closely tied to the concept of closures.

A closure is a function bundled together with its surrounding state. In other words, a closure allows a function to access variables from the scope in which it was created, even after that scope has finished executing.

Closures are created when you define a function inside another function:

function outerFunction() {
const message = ‘Hello, world!‘;

function innerFunction() {
console.log(message);
}

return innerFunction;
}

const myFunction = outerFunction();
myFunction(); // logs "Hello, world!"

outerFunction() returns its inner function, which is assigned to the variable myFunction. Even though outerFunction() has finished running, the inner function still has access to the message variable from its original scope.

This technique of a function "remembering" its creation environment can be used to emulate private state:

function counter() {
let count = 0;

return function() {
count++;
console.log(count);
};
}

const increment = counter();
increment(); // logs 1
increment(); // logs 2

Each time counter() is called, a new scope is created with a new count variable. This variable can only be accessed by the returned inner function. Calling increment() multiple times increments its own private count variable, which persists between invocations.

Closures allow for some interesting functional programming patterns. One example is memoization, which involves caching the results of expensive function calls to avoid repeated computation:

function memoize(fn) {
const cache = {};

return function(...args) {
if (cache[args]) {
return cache[args];
}

const result = fn(...args);
cache[args] = result;

return result;

};
}

function slowFunction(num) {
console.log(‘Calling slow function‘);
return num * 2;
}

const fastFunction = memoize(slowFunction);

console.log(fastFunction(2)); // logs "Calling slow function" and 4
console.log(fastFunction(2)); // logs 4 (cached result)

memoize() is a higher order function that takes a function as an argument and returns a new function. The returned function checks if the arguments have been seen before. If so, it returns the cached result. Otherwise, it calls the original function, caches the result, and returns it.

When fastFunction(2) is called the first time, slowFunction() is executed and its result is cached. The second time, the cached result is returned, avoiding the need to recompute the value. This can greatly speed up recursive functions like factorial or Fibonacci.

Another example is partial application, which involves fixing some arguments to a function to produce a new function with fewer arguments:

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

function add(a, b) {
return a + b;
}

const add10 = partial(add, 10);

console.log(add10(5)); // logs 15
console.log(add10(12)); // logs 22

partial() takes a function and any number of arguments to "fix". It returns a new function that takes the remaining arguments and calls the original function with both the fixed and remaining arguments.

By calling partial(add, 10), we create a new function add10() that always passes 10 as the first argument to add(). This lets us specialize add() for common use cases.

Creating Your Own Higher Order Functions

Now that we understand the basics of passing functions as arguments and returning functions from other functions, let‘s see how we can combine these techniques to write our own higher order functions from scratch.

We‘ll start by re-implementing some of the common array methods like map(), filter(), and reduce(). This will help us understand how they work under the hood.

Here‘s our version of map():

function map(array, callback) {
const result = [];

for (let i = 0; i < array.length; i++) {
result.push(callback(array[i]));
}

return result;
}

const numbers = [1, 2, 3, 4, 5];
const doubled = map(numbers, n => n * 2);

console.log(doubled); // [2, 4, 6, 8, 10]

map() takes an array and a callback function as arguments. It creates a new array, loops through the input array, calls the callback on each element, and pushes the result to the new array. Finally, it returns the new mapped array.

Here‘s filter():

function filter(array, callback) {
const result = [];

for (let i = 0; i < array.length; i++) {
if (callback(array[i])) {
result.push(array[i]);
}
}

return result;
}

const odds = filter(numbers, n => n % 2 !== 0);
console.log(odds); // [1, 3, 5]

filter() works similarly to map(), but it only pushes elements to the result array if the callback returns a truthy value for them. This way, we can filter out elements that don‘t pass the test implemented by the callback.

Finally, here‘s reduce():

function reduce(array, callback, initialValue) {
let accumulator = initialValue;

for (let i = 0; i < array.length; i++) {
accumulator = callback(accumulator, array[i]);
}

return accumulator;
}

const sum = reduce(numbers, (acc, curr) => acc + curr, 0);
console.log(sum); // 15

reduce() takes an array, a callback function, and an optional initial value as arguments. It loops through the array and calls the callback on an accumulator and the current element. The accumulator is initialized to the initial value if provided, or the first element of the array otherwise. Each iteration updates the accumulator to the return value of the callback. After the last iteration, the accumulator is returned as the final result.

Once you get comfortable with higher order functions, you can start composing them together to create powerful data processing pipelines:

const numbers = [1, 2, 3, 4, 5];

const sumOfSquaredEvens = numbers
.filter(n => n % 2 === 0)
.map(n => n ** 2)
.reduce((acc, curr) => acc + curr);

console.log(sumOfSquaredEvens); // 20

Here we‘re chaining filter(), map(), and reduce() to filter out odd numbers, square the remaining even numbers, and compute the sum of the squares. The result of each operation is passed as the input to the next, allowing us to succinctly express complex logic in a readable way.

Composition is the essence of functional programming. By writing small, focused functions that each do one thing well, we can combine them in powerful ways to solve more complex problems. Higher order functions are the glue that allows this composition to happen.

Best Practices and Pitfalls

Higher order functions are an extremely valuable tool, but like any tool, they can be misused. Here are some best practices to keep in mind:

  • Keep your functions small and focused on a single task. This makes them more reusable and easier to reason about.
  • Avoid side effects in your callbacks. A side effect is any change to the external environment, like modifying a global variable or making an API request. Side effects make functions unpredictable and harder to test.
  • Use pure functions as much as possible. A pure function always returns the same output for the same input and has no side effects. This makes them deterministic and predictable.
  • Be mindful of variable scope and closures. It‘s easy to accidentally capture variables you didn‘t intend to, leading to subtle bugs.
  • Don‘t overuse higher order functions. Sometimes a regular loop is more readable than chaining a bunch of array methods together. Use your best judgment.

Higher order functions are not always the right tool for the job. In general, they are most effective when you need to:

  • Process collections of data (arrays, objects) in a reusable way
  • Implement the decorator pattern to modify existing behavior
  • Create utilities for functional programming techniques like composition and partial application
  • Handle asynchronous code using callbacks or promises

On the other hand, higher order functions may be overkill for simple or performance-critical code. As with any abstraction, they come with a slight overhead that may be noticeable in hot paths.

Conclusion

In this guide, we‘ve covered the fundamentals of higher order functions in JavaScript. We learned that:

  • Higher order functions are functions that take other functions as arguments or return functions as their result
  • JavaScript supports higher order functions because it treats functions as first-class citizens
  • Callback functions are the most common way to pass functions as arguments
  • Returning functions from other functions allows for powerful patterns like closures, memoization, and partial application
  • We can implement our own higher order functions like map(), filter(), and reduce()
  • Composition is the key to writing modular, reusable code with higher order functions

If you‘re still hungry for more, here are some additional resources to level up your functional programming skills:

Higher order functions are a powerful concept that every JavaScript developer should master. By understanding how to use them effectively, you can write cleaner, more modular, and more expressive code. So go forth and start writing some higher order functions of your own!

Similar Posts