A Guide to Higher-Order Functions in JavaScript

Photo by Growtika on Unsplash

A Guide to Higher-Order Functions in JavaScript

If you work with JavaScript, then you might have encountered higher-order functions. This is not to mean that they are only limited to JavaScript alone. On the contrary higher-order functions are a fundamental aspect of functional programming that can be found in various programming languages.

The ability to use higher-order functions is more linked with the language’s support for functional programming and the programming paradigm in play.

In this article we’ll dig deeper into what higher-order functions are, their use case scenarios, benefits, and some common mistakes encountered while using them in JavaScript.

What is a higher-order function?

A higher-order function is a function that takes one or more functions as arguments or returns a function as its result.

In that definition lies the key characteristics of identifying a higher-order function:

  • Takes a function as an argument.

  • Returns a function.

This whole concept of higher-order functions in programming is borrowed from mathematics, specifically from the field of functional programming and lambda calculus (a little nerdy note 😁).

In lambda calculus and mathematical logic, functions are considered first-class citizens, meaning they can be treated as values, passed as arguments to other functions, and returned as results from functions.

For example, the derivative operator in calculus is a higher-order function that maps a function to its derivative, which is also a function. Similarly, in JavaScript, filter() is a higher-order function that works on an array and takes another function as an argument that it’ll use to return a new filtered version of the array.

Now that we know what higher-order functions are, let’s move on to their applications:

Common Use Cases

1. Callback functions

A callback function is simply a function that is passed as an argument to another function and is executed inside the outer function to complete some routine or action.

This technique allows a function to call another function.

Callback functions are commonly used in asynchronous programming to handle tasks that may take time to complete such as a network request or handling an event for example a button click or a timer expiration.

Let’s implement a simple instance of callbacks in play within an asynchronous task:

// Asynchronous application of a callback function

function getDataFromServer(callback) {
  // Simulating a server request with a setTimeout
  setTimeout(function() {
    const data = 'Data from the server';
    callback(data);
  }, 2000);
}

// Callback function to handle the data
function displayData(data) {
  console.log('Data received: ' + data);
}

// Calling the function with the callback
getDataFromServer(displayData);

In this example, we use the getDataFromServer function to simulate a server request using setTimeout and then call the callback function with the data after 2 seconds. The displayData function is the callback that handles the received data. When getDataFromServer is called with displayData as the callback, it logs the received data after 2 seconds.

Output on replit

This demonstrates the asynchronous nature of the callback, as the data is handled after the server request is complete.

2. Array Methods

Array methods are functions that can be called on an array object to perform various operations on the array.

They are quite a number but not all of them are higher-order functions it should be noted. The higher-order array methods are:

  • forEach()

  • map()

  • sort()

  • filter() and

  • reduce()

Let’s take a quick look at each:

1. forEach()

This is a higher-order function that iterates over each item of an array executing the function passed in as an argument for each item of the array. Illustration:

const numbers = [1, 2, 3, 4]; 
numbers.forEach((num) => console.log(num));
 // Output: 1, 2, 3, 4

2. map()

This higher-order function returns a new array based on the results from the function passed in as an argument and called on each element of the original array.

Illustration:

const numbers = [1, 2, 3, 4, 5];  
const doubledNumbers = numbers.map((num) => num * 2);  
console.log(doubledNumbers);
 // Output: [2, 4, 6, 8, 10]

Each element from the numbers array is multiplied by 2 to create a new array through the map() method.

3. sort()

As the name suggests when called on an array it sorts it and returns it in default ascending order. Under the hood though, it changes each element into a string and compares their sequence.

You could use sort without passing a function in as an argument, however, this only works well for string arrays but not for numbers.

Hence if you’re going to use sort on numbers you pass in a compare function as an argument. The compare function takes 2 arguments. Illustration:

const numbers = [6, 9, 3, 1]; 
numbers.sort((a, b) => a - b); 
// After sorting: [1, 3, 6, 9]

In the code above:

The negative value (a < b) => a will be placed before b

The zero value (a== b) => No change

The positive value (a > b) => a will be placed before b

4. filter()

This method returns a new array with all elements that satisfy the condition implemented by the function passed. The function passed should always return a boolean value.

const numbers = [1, 2, 3, 4]; 
const evenNumbers = numbers.filter((num) => num % 2 === 0); 
// Result: [2, 4]

The condition in the body of the passed in function is evaluated for each element in the numbers array to return a new array of only values that satisfy it.

5. reduce()

This method takes in 2 arguments a callback function and an initial value. The callback function takes 2 parameters an accumulator (which is set to the initial value at the start) and current value that run on each element in the array to return a single value in the end.

Illustration:

const numbers = [1, 2, 3, 4, 5]; 
const sum = numbers.reduce(function(acc, num) { 
                              return acc + num; 
                           }, 0);

console.log(sum); 
// Output: 15

The value of acc is 0 and that of num is the first item of the array at the start of execution. As reduce iterates over the array the acc value changes to the value returned after summation. This new value of acc is then applied to the next iteration. This continues until the end of the array.

3. Functional composition

Functional composition is the act of combining multiple smaller functions by chaining them together to obtain a single more complex function.

In this approach, a function in the chain operates on the output of the previous function in the composition chain.

To enable functional composition in JavaScript we use the compose function. A compose function is a higher-order function that, takes in two or more functions and returns a new function that applies these functions in right-to-left order.

Let's dive into an example:

  • The first step is to define the compose function:
const compose = (...functions) => {
     return (input) => {
         return functions.reduceRight((acc, fn) => {
             return fn(acc);
         }, input);
     };
   };

In the above implementation, the compose function takes in any number of functions as arguments by using the spread operator.

It returns a new function that iterates over the functions in reverse order using reduceRight, applying each function to the accumulated result.

  • The next step is to now use the compose function:
const addOne = x => x + 1; 
const multiplyByTwo = x => x * 2;
const addOneAndMultiplyByTwo = compose(multiplyByTwo, addOne); 
console.log(addOneAndMultiplyByTwo(5)); 
// Output: 11

Above the entered value is first multiplied by 2 then added a one by applying the functions passed to compose from right to left.

Benefits of Higher Order Functions

We have seen that there are quite several applications for higher-order functions. This indicates that they do offer some benefits to programmers to warrant their use.

Let’s take a look at these benefits:

  • Abstraction: Higher order functions allow you to hide the implementation details behind clear function names improving the readability and understanding of code. For instance, without this type of functions iterating over arrays would need the use of loops which can clutter code, especially in a large codebase, and make it more complicated.

  • Reusability: Higher-order functions encapsulate the same logic into reusable functions. This means they can be reapplied throughout the codebase without the need to create them from scratch at every instance.

  • Immutability: Many higher-order functions operate on data immutably, creating new structures instead of modifying existing ones. This helps avoid unintended side effects and makes your code more predictable and easier to debug.

  • Modularity: Higher-order functions facilitate breaking down complex problems into smaller, manageable parts, promoting modular design.

Common mistakes when working with Higher Order Functions (HOFs)

Higher-order functions do offer numerous benefits as seen above, but it’s essential to be aware of the potential pitfalls that can cause bugs and unexpected results.

Some common mistakes to avoid:

  • Not returning a value: Most HOFs like map, filter and reduce expect the function passed in as an argument to return a value for each element processed. In this instance forgetting to return a value can disrupt the chain and lead to undefined results.

  • Not using the correct syntax: HOFs can be complex and it’s easy to make syntax errors when creating or using them. A good rule of thumb is to double-check the syntax and ensure functions are properly defined and called.

  • Not using pure functions: HOFs should not cause any side effects thus they should return the same output for a given input, to be pure functions. Using them in an impure manner could lead to unexpected behavior that makes debugging a nightmare.

  • Overusing composition: Functional composition is a powerful programming technical but excessive nesting of HOFs can increase the complexity of code making it harder to read and understand. Aim for a balance between conciseness and clarity, breaking down complex transformations into smaller, more readable steps if necessary.

Conclusion

As we reach the end, some key takeaways are:

  1. Higher-order functions are functions that either take other functions as arguments or return them as output.

2. Higher-order functions do have a lot of use cases some of them being in:

  • Callback functions,

  • Array methods,

  • Functional composition etc..

3. Higher-order functions benefit programmers by providing:

  • Abstraction

  • Reusability

  • Immutability

  • Modularity

4. As programmers there are common mistakes that we may encounter when working with Higher-Order Functions.

I hope this blog inspires a deeper understanding of higher-order functions and how you might be able to leverage them to solve real-world problems. The journey of learning is never-ending. Happy coding!