Higher-Order Functions(HoF) in JavaScript - Explain Like I'm Five
A Higher-Order function is a widely used functional programming concept in JavaScript. Let's learn what they are and their benefits with examples.
Table of contents
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');
});
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
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];
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;
}
So, if we do,
incrArr(data, 2);
Output,
[14, 5, 52]
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;
}
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;
}
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;
}
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;
}
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 increment or decrement.
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);
Output,
[15, 6, 53]
How about trying the decrement operation now?
const data = [12, 3, 50];
const result = smartOperation(data, decr, 2);
console.log(result);
Output,
[10, 1, 48]
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;
}
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);
Output,
[36, 9, 150]
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]
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.
You can find all the source code used in the article in this stackblitz project.
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,