How to use Reduce in JavaScript

Paweł Puzio
9 min readJan 27, 2022
Photo by Photoholgic on Unsplash

Why should you care about reduce? It’s a powerful method that allows you to perform complex transformations without writing too much code. This article will explain some of the intricacies of this functionality and hopefully will make you wish you’d been using it more before.

Most commercial programmers will sooner or later stumble upon a term called Declarative Programming. It’s a style that most high schools and universities don’t teach, focused on readability and telling you what is done instead of describing how things are implemented.

Oftentimes, when following the declarative approach, we’ll use inbuilt methods like the ones featured in Array.prototype or provided by an external library, like ramda or lodash — in such case we don’t know how the function was implemented exactly, but usually, it’s not a problem.

Most of these methods are very straightforward — for example Array.prototype.some checks if there’s an element that fulfills our requirements and returns either true or false. Array.prototype.map creates a new array that features all previous elements modified according to the function we passed.

But there’s one method that also confuses experienced developers — Array.prototype.reduce. In an attempt to demystify it for you, we’ll use it to implement a basic version of three popular Array.prototype methods and one Object.prototype method that takes an array as the input. Step-by-step solutions will make sure that also beginners will understand the solution and Jest tests provided in the repository will prove that the newly created methods work as they should.

Disclaimer: these are not meant to be used as production ready polyfills — I tried to cover as many cases as possible while making sure the examples are not too complicated.

Some of you will ask: what’s the fuss about reduce?

source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce#Browser_compatibility

JavaScript keeps evolving; however, some developers still have to support legacy projects. If one wants to use the newest functionalities while e.g. supporting IE9, they’ll have to resort to polyfilling (creating an implementation of a feature for an environment that doesn’t support it). The reduce function works great in that matter.

Let’s look at the function itself quickly. The way it’s built is both the strongest point and the biggest obstacle when approaching it at first. The function itself accepts two parameters — a reducer function and an initial value, which is optional.

Array.reduce(reducerFunction, initialValue)

We’ll talk about the reducer function in a moment, let’s focus on the initial value first. Lots of people will wonder: what makes reduce so powerful? One of the answers lies in the initial value itself. Other array methods return a specific type — map, filter, flat, etc. will return an array; some, every return a Boolean (true/false).
Reduce allows us to return any type of value, and the factor that determines the type is our initial value — it will yield different results when provided with different initial values. Mindblowing?

The reducer function itself accepts four parameters.

  • accumulator: the value that will eventually be returned after the execution, which accumulates the values. In the first step, the accumulator will be equal to initialValue,
  • current value — the meaning of current in this context will be explained in a moment,
  • current index — as above,
  • the original (input) array.

Warmup example

Let’s start with an easy task: we’re given an array of numbers that we want to add together. The imperative approach that’s taught in high schools would probably make use of a for loop. We’d iterate over every element of the array and add that element to a helper variable. And that’s where the non-technical folks will get confused.

Programmers know this syntax by heart. But non-technical people don’t.

This approach demands some knowledge already. While it is good for CS101, at the same time it’s a bit too verbose for what it does. One needs to know the role of i++, what the [i] after array stands for, and why i was invoked in the first place. Let’s see how reduce could help us here.

Isn’t code easier to read when it’s written like a book?

Let’s see what this function does, step by step. Meet console.log, a function that allows us to output each step of the process. We’ll add the aforementioned third reducer variable, index, to show you what happens with each iteration.

The first index of an array equals 0 and the last equals the array’s length minus one. Hence the +1, we’ll stop using it later. Just remember that the indexes in an array with three elements are 0, 1, 2.

Notice that the value which we return with each iteration becomes the new accumulator. Notice that, as mentioned before, the accumulator is equal to the initial value in the first iteration.

Since Reduce 101 is behind us, let’s get to work.

map

So how do we recreate Array.prototype.map using reduce? What map does is:

· it takes each element and applies the passed function to it,

· it then returns a new array with the modified elements.

Let’s say we have an array of numbers and we’d want to multiply each by three. We can achieve it with the following function:

We’re using the arrow function expression syntax here, thanks to which we don’t have to use curly braces and return for short functions. Each number is multiplied by 3 and returned. The lengths of arrays before and after are equal. How do we achieve that with reduce?

What we need to do is to:

· create an array — the initial value has to be an empty array or the concat function will throw an error,

· take elements of the input array and modify them according to the passed function (we’ll call functions passed as parameters callbacks from now on),

· add the modified elements to our accumulator array, which will then be returned.

Concat is an Array.prototype method that allows us to merge an existing array with another one or pass values to it. Code with tests can be accessed here.

filter

Filter returns a new array including the elements that fulfill our requirement. How do we achieve that? By creating a filtering function that returns true when the requirements are met.

What we essentially do is:

· create an array,

· take elements from the input array and inspect them with the provided function,

· when the result of passing the current value to the filtering callback function is true, the value is added to the accumulator, otherwise we leave the accumulator as it is.

Code with tests can be accessed here.

What’s the deal with this question mark and colon, all of a sudden?

one might ask. The ternary operator is a shortcut for a traditional if/else conditional statement. If the value passed before the question mark (here callback(currentValue)) is truthy, it returns the value after the question mark — in this case, the concatenation of the accumulator and current value, otherwise the value after the colon, which is the accumulator itself.

It won’t be an exaggeration to say that the TC39 committee and everyone involved in supporting the new ECMA features are heroes to all JavaScript developers by constantly providing functionalities that make our work easier. I promised to provide a way to use new, unsupported functionalities — both Array.prototype.flat and Object.fromEntries are powerful but unsupported in IE.

fromEntries

To know what fromEntries does, it’s best to first see how Object.entries works.

The object is transformed into an array of arrays (yikes), each of them holding a pair of key and value. How do we go back to the original object? The more fortunate ones by using Object.prototype.fromEntries():

The legacy supporters will have to find another way to achieve that. How?

We see that for simple objects, the output of entries is always the same. The first element in the array is the index, the second is the value. You can read about the dot syntax that we use in the example below here.

The first approach is the more descriptive one, whereas the second one explicitly returns a new object. When returning objects, we have to wrap them in parentheses so they don’t get interpreted as a function. We also have to surround dynamically generated indexes in parentheses, like in lines 3 and 15.

But what about deeply nested objects?

Since Object.entries doesn’t transform nested objects into arrays, our algorithm is also prepared for cases of nested data. Code with tests can be accessed here.

flat

Just FYI — this implementation will be longer and more detailed than the previous ones. Don’t think less of yourself if you don’t understand it at first — it also features the thought process that’s necessary to achieve a solution that covers most cases and introduces some tricks.

Array.prototype.flat is an interesting function — as the name suggests, it will flatten (reduce the depth) of the array by as many times as we intend.

If no argument is passed to Array.prototype.flat, then the array is flattened by one level. To make sure that any array will be maximally flattened we call flat(infinity). This one will be the most confusing of them all, so fasten your seatbelts. The first attempt looks as follows (don’t worry, we’ll improve it):

There are more declarative solutions with fewer lines but their readability is much lower.

So, we’ve got the flattening of undefined and numbers down. But what about other values? That’s where it gets dirty.

What I found out is that that flat treats all falsy values other than undefined and all other values in general other than numbers bigger than 0 and a Date object as it would with flat(0). As you’ve probably guessed, our script isn’t ready for that (yet).

Insert the“It’s getting harder” meme

We’re performing lots of checks in here:

  • checking whether times is falsy but not undefined,
  • checking whether times is an empty object,
  • checking whether times is an empty array,
  • checking whether times is a number below zero,
  • checking whether times is a nonempty string.

In any of these cases, we should return the original array. And that covers most of the cases, albeit rather gruesomely.

There’s one (actually, two) problem — if times is equal to Number.POSITIVE_INFINITY then our do/while loop will never stop iterating. Additionally (that’s the second problem), Date objects are treated as an integer by the native flat function, which interprets them as the number of milliseconds since midnight, January 1, 1970 (around 1.64 * 10¹² as of 19.01.2022).
In both of these cases, we want to somehow check if the loop still has to keep going. Additionally, it seems that there are fewer cases where we actually want to perform the flattening than where we want to return the input array, so we’ll simplify the first check as well.

It’s kind of a dirty hack but it does the job — basically, if we try to see if values such as an object, array, etc. are greater than zero, we’ll get false in return.

Okay, but how do we know for certain that our function will yield the same results as the native one? That’s where unit tests come in handy. Since these tests are a bit more complex than the previous ones, they’ll be described step by step.

We need to create some data that we’ll use to verify if our function works properly. This means that we have to cover several cases of depth and irregular nesting.
First, we create some methods and variables that will help us test edge cases and create data.

Our first method arrayOfNumbers creates an array of numbers, starting from 1 up until the number that we provide as length, so calling it with 3 will return [1,2,3], etc. Then we create an array that we’ll use to check if a very deeply nested array has been correctly flattened.
The last variable and function might look scary, so let me explain them with an example.

Our nestedArrayGenerator function uses reduceRight, which basically works like reduce, but starts iterating from the last element. In each iteration, the function creates an array where the first element is the current element, and the second element is the accumulator array. If the accumulator array is empty (first step), then we just insert the current element inside.

Afterward, I created some more data using a different nesting pattern — the first group of variables follows a symmetrical nesting pattern, and the second one follows a wavy one.

Then we create cases for our tests to check — each of these cases will be an array, containing the input array that we provide to our flat function, the depth argument that we’ll use, and our expected output array. I also provided cases that can make our function crash.

The last missing piece is the actual test itself. I used thetest.each function provided by Jest, but it’s also possible to use forEach on the cases array.
The second each call accepts two parameters: a string that explains the case which is tested and an assertion function that will perform the actual test. Each %p in the first string represents an argument in the assertion function.

In line 5 we create a result of using the in-built flat function, and in line 6 we create a result of using our custom function. Then we check if both arrays are equal to the array that we’re expecting.

Code with tests can be found here.

As you can see, we managed to achieve quite a lot thanks to the power of reduce (and do/while in the last example, I’ve got to admit the imperative style came in handy). I hope you’ve learned how powerful reduce is, that the immediate intimidation is gone and that you’ll start using it in your projects!

--

--

Paweł Puzio

Software Engineer (React/Node/WASM) @ CodeComply.ai. I’m all about React, traveling, foreign languages, and photography.