Higher-Order Functions(HoF) in JavaScript - Explain Like I'm Five

atapas

Tapas Adhikary

Posted on December 28, 2021

Higher-Order Functions(HoF) in JavaScript - Explain Like I'm Five

JavaScript Functions

Functions are an integral part of many programming languages, and JavaScript is not an exception. In JavaScript, functions are the first-class citizens. You create them, assign them as a value, pass them as arguments to other functions, also return them as a value from a function.

These flexibilities help in code reusability, clean code, and composability. Today we will learn about Higher-Order Functions to use functions to their full potential in JavaScript.

If you like to learn from video content as well, this article is also available as a video tutorial here: 🙂

Don't forget to subscribe for the future content.

What are Higher-Order Functions?

A Higher-Order Function is a regular function that takes one or more functions as arguments and/or returns a function as a value from it.

Here is an example of a function that takes a function as an argument.

// Define a function that takes a function as an argument.
function getCapture(camera) {
  // Invoke the passed function
  camera();
}

// Invoke the function by passing a function as an argument
getCapture(function(){
  console.log('Canon');
});
Enter fullscreen mode Exit fullscreen mode

Now let us take another function that returns a function.

// Define a function that returns a function
function returnFunc() {
  return function() {
    console.log('Hi');
  }
}

// Take the returned function in a variable.
const fn = returnFunc();
// Now invoke the returned function.
fn(); // logs 'Hi' in the console

// Alternatively - A bit odd syntax but good to know
returnFunc()(); // logs 'Hi' in the console
Enter fullscreen mode Exit fullscreen mode

Both of the examples above are examples of Higher-Order functions. The functions getCapture() and returnFunc() are Higher-Order functions. They either accept a function as an argument or return a function.

Please note, it is not mandatory for a Higher-Order function to perform both accepting an argument and returning a function. Performing either will make the function a Higher-Order function.

Why use Higher-Order Functions? How to create Higher-Order Functions?

So, we understand what a Higher-Order function is. Now, let us understand why we need one and how to create it? How about doing it with a few simple examples.

The Problem: Code Pollution and Smell

Let's take an array of numbers,

const data = [12, 3, 50];
Enter fullscreen mode Exit fullscreen mode

Now let's write code to increment each array element by a number and return the modified array. You may think about writing it as a function.

function incrArr(arr, n) {
  let result = [];

  // Iterate through each elements and
  // add the number
  for (const elem of arr) {
    result.push(elem + n);
  }

  return result;
}
Enter fullscreen mode Exit fullscreen mode

So, if we do,

incrArr(data, 2);
Enter fullscreen mode Exit fullscreen mode

Output,

[14, 5, 52]
Enter fullscreen mode Exit fullscreen mode

Great so far. Now, if I ask you to write code to decrement each of the elements of the data array by a number and return the modified array? You may think about solving it in a couple of straightforward ways. First, you can always write a function like,

function decrArr(arr, n) {
  let result = [];

  for (const elem of arr) {
    result.push(elem - n);
  }

  return result;
}
Enter fullscreen mode Exit fullscreen mode

But that's lots of code duplication. We have written almost every line of the incrArr() function in the decrArr() function. So, let's think about the reusability here.

Now, you may want to optimize the code to have one single function performing both these operations conditionally.

function doOperations(arr, n, op) {
  let result = [];

  for (const elem of arr) {
    if (op === 'incr') {
      result.push(elem + n);  
    } else if (op === 'decr') {
      result.push(elem - n);
    }
  }

  return result;
}
Enter fullscreen mode Exit fullscreen mode

So, now we rely on a third argument to decide if the operation is to increment or decrease the array's number. There is a problem too. What if I ask you to multiply each element of an array by a number now? You may think about adding another else-if in the doOperations() function. But that's not cool.

For every new operation, you need to change the logic of the core function. It makes your function polluted and will increase the chance of code smells. Let's use the Higher-Order function to solve this problem.

The Solution: Higher-Order Function

The first thing to do is create pure functions for the increment and decrement operations. These functions are supposed to do only one job at a time.

// Increment the number by another number
function incr(num, pad) {
  return num + pad;
}

// Decrement the number by another number
function decr(num, pad) {
  return num - pad;
}
Enter fullscreen mode Exit fullscreen mode

Next, we will write the Higher-Order function that accepts a function as an argument. In this case, the passed function will be one of the pure functions defined above.

function smartOperation(data, operation, pad) {
  // Check is the passed value(pad) is not a number.
  // If so, handle it by assigning to the zero value.
  pad = isNaN(pad) ? 0 : pad;

  let result = [];
  for (const elem of data) {
    result.push(operation(elem, pad));
  }
  return result;
}
Enter fullscreen mode Exit fullscreen mode

Please observe the above function closely. The first parameter is the array to work on. The second parameter is the operation itself. Here we pass the function directly. The last parameter is the number that you want to increament or decerement.

Now, let's invoke the function to increment array elements by three.

const data = [12, 3, 50];
const result = smartOperation(data, incr, 3);
console.log(result);
Enter fullscreen mode Exit fullscreen mode

Output,

[15, 6, 53]
Enter fullscreen mode Exit fullscreen mode

How about trying the decrement operation now?

const data = [12, 3, 50];
const result = smartOperation(data, decr, 2);
console.log(result);
Enter fullscreen mode Exit fullscreen mode

Output,

[10, 1, 48]
Enter fullscreen mode Exit fullscreen mode

Did you notice that we didn't make any changes to our function to accommodate a new operation this time? That's the beauty of using the Higher-Order function. Your code is smell-free and pollution-free. So, how do we accommodate a multiplication operation now? Easy, let's see.

First, create a function to perform multiplication.

function mul(num, pad) {
  return num * pad;
}
Enter fullscreen mode Exit fullscreen mode

Next, invoke the Higher-Order function by passing the multiplication operation function, mul().

const data = [12, 3, 50];
const result = smartOperation(data, mul, 3);
console.log(result);
Enter fullscreen mode Exit fullscreen mode

Output,

[36, 9, 150]
Enter fullscreen mode Exit fullscreen mode

That's incredible. Long live Higher-Order functions.

In-built Higher-Order Functions in JavaScript

In JavaScript, there are plenty of usages of higher-order functions. You may be using them without knowing them as Higher-Order functions.

For example, take the popular Array methods like, map(), filter(), reduce(), find(), and many more. All these functions take another function as an argument to apply it to the elements of an array.

Here is an example of the filter() method that filters the array elements based on the condition we pass to it as part of the function argument.

const data = [1, 23, 45, 67, 8, 90, 43];

const result = data.filter(function(num){
    return (num % 2 === 0);
});

console.log(result); // [8, 90]
Enter fullscreen mode Exit fullscreen mode

Higher-Order Functions vs Callback functions

There is always some confusion between the Higher-Order functions and callback functions. Higher-Order Functions(HoF) and Callback Functions(CB) are different.

  • Higher-Order Functions(HoF): A function that takes another function(s) as an argument(s) and/or returns a function as a value.
  • Callback Functions(CB): A function that is passed to another function.

Conclusion

To conclude, the Higher-Order function is a fundamental concept built in the JavaScript language. We need to find opportunities to leverage it as much as possible in our coding practices. Higher-Order function in conjunction with the pure function will help you keep your code clean and side effects free.

I will leave you with this article on Pure Function and Side Effects in JavaScript. I hope you enjoy reading it as well.

What are pure functions and side effects in javascript

You can find all the source code used in the article in this stackblitz project.

https://stackblitz.com/edit/learn-js-hof



I hope you found this article insightful. Thanks for reading. Please like/share so that it reaches others as well.

Let's connect. I share my learnings on JavaScript, Web Development, Career, and Content on these platforms as well,

💖 💪 🙅 🚩
atapas
Tapas Adhikary

Posted on December 28, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related