paint-brush
The Superpowers of Array.reduce() Methodby@gilad-bar
406 reads
406 reads

The Superpowers of Array.reduce() Method

by Gilad BarJanuary 11th, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

ECMAScript 5 introduced array methods like isArray, forEach, map, filter, every, some. But let’s talk about my favorite one: the awesomereduce method. It executes a callback function (provided by the user) on each element of the array, resulting in a single output value. The return value is assigned to the accumulator, whose value is remembered across each iteration throughout the array and ultimately becomes the final, single resulting value. It allows you to take an array and reduce its values to basically anything that can be derived from data it holds.

Coin Mentioned

Mention Thumbnail
featured image - The Superpowers of Array.reduce() Method
Gilad Bar HackerNoon profile picture

ECMAScript 5 introduced many awesome features in 2009, the majority of them being array methods like isArrayforEachmapfiltereverysome. But let’s talk about my favorite one: 

reduce
.

The
reduce
method

The 

reduce
 method executes a 
reducer
 callback function (provided by the user) on each element of the array, resulting in a single output value.

reducer

The 

reducer
 function takes four arguments:

  • Accumulator (acc)
  • Current value (cur)
  • Current index (idx)
  • Source array (src)

Your 

reducer
 function’s return value is assigned to the accumulator, whose value is remembered across each iteration throughout the array and ultimately becomes the final, single resulting value.

Important: On each iteration, you must return the accumulator value for the next iteration (which will eventually be the final return value), or else the accumulator’s next (and ultimately final) value will be 

undefined
.

initialValue

The 

reduce
 method takes a second optional argument: 
initialValue
.
If not provided, the initial value of the accumulator will be the first element of the array, and the first iteration will point to the second element. If 
initialValue
 is provided, it will be the initial value of the accumulator, and the first iteration will point to the first element of the array.

Examples

Summing numbers with/without an initial value

const numbers = [1, 2, 3];

// Without initialValue
const sum = numbers.reduce(
  (accumulator, currentValue) => accumulator + currentValue
);

// Prints 6
console.log(sum);

// With initialValue
const initialValue = 3;
const sumWithInitialValue = numbers.reduce(
  (accumulator, currentValue) => accumulator + currentValue
, initialValue);

// Prints 9
console.log(sumWithInitialValue);

Without the initial value, the first iteration will have 

accumulator
 pointing to the first element of the array (1), and 
currentValue
 pointing to the second element of the array (2).

Given the initial value, the first iteration will have an 

accumulator
 with the value of the given initial value (3), and 
currentValue
 will point to the first element of the array (1).

Counting the number of occurrences in an array

Let’s count the number of occurrences of words in the following and store the results in a map:

How much wood would a woodchuck chuck
If a woodchuck could chuck wood?
He would chuck, he would, as much as he could,
And chuck as much as a woodchuck would
If a woodchuck could chuck wood.
const sentence = "how much wood would a woodchuck chuck" +
  "if a woodchuck could chuck wood " +
  "he would chuck he would as much as he could " +
  "and chuck as much as a woodchuck would " +
  "if a woodchuck could chuck wood";

const words = sentence.split(" ");

const occurencesMap = words.reduce(
  (occurences, word) => {
    const numOfOccurences = (occurences.get(word) || 0) + 1;
    occurences.set(word, numOfOccurences);
    return occurences;
  }
, new Map());

const numOfWoodchucks = occurencesMap.get("woodchuck");

// 4
console.log(numOfWoodchucks);

We initialize an empty map and use it as the initial value of the accumulator, initializing or updating the number of occurrences of each word as we iterate over the words in the sentence.

These are only two examples, but by now you must have realized how awesome 

reduce
 is, right?

It allows you to take an array and reduce its values to basically anything that can be derived from the data it holds. It also allows you to return any type of data, regardless of the type of the elements of the array.

One Method to Rule Them All?

Revisiting other ES5 array methods, we can see that each method uses the given callback function on the array and returns some kind of result.

For example:

  • map
     transforms each element of the array, returning a new array.
  • every
     checks if the given condition applies to every element in the array, returning the corresponding Boolean value.

Looks familiar, right?

Using what we already know, let’s try to use 

reduce
 to implement other ES5 array methods.

Note: we’ll add the new methods to 

Array
’s prototype in each example, where 
this
 will point to the array on which we’re operating.

map

The map() method creates a new array where each original element is transformed by the given 

transformer
 callback.

Usage

const array = [1, 2, 3];
const doubled = array.map(num => num * 2);

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

Using a transformer callback that doubles every number in the array, we get a new array where every element is twice its original value.

With 
reduce

map
 operates on an array and returns a new array, so the accumulator has to be an array.

Array.prototype.mapWithReduce = function(transformer) {
  return this.reduce((newArray, currentElement) => {
    const newElement = transformer(currentElement);
    newArray.push(newElement);
    return newArray;
  }, []);
}

const array = [1, 2, 3];
const doubled = array.mapWithReduce(num => num * 2);

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

Using 

reduce
, we start with an empty array accumulator and iterate over the array. We then apply the transformer callback on each element and push it to the accumulating array.

filter

The filter() method creates a new array with all elements that pass the test implemented by the provided function.

Usage

const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenOnly = array.filter(num => num % 2 === 0);

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

Using a test callback that filters out all odd numbers, we get a new array with all the even elements of the original array.

With 
reduce

Just like the previous example, filter also operates on an array and returns a new array, so the accumulator has to be an array.

Array.prototype.filterWithReduce = function(tester) {
  return this.reduce((newArray, currentElement) => {
    if (tester(currentElement)) {
      newArray.push(currentElement);
    };
    return newArray;
  }, []);
}

const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenOnly = array.filterWithReduce(num => num % 2 === 0);

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

Using 

reduce
, we start with an empty array accumulator and iterate over the array. We then use the tester callback to check if each element should be pushed to the accumulating array.

every

The every() method tests whether all elements in the array pass the test implemented by the provided function. It returns a Boolean value.

Usage

const array = [1, 2, 3, 4, 5];
const result = array.every(num => num < 10);

// Prints true
console.log(result);

Using a callback function that tests every element in the array, we get a boolean that indicates whether all elements pass the test. In this case, all elements are smaller than 10, and thus 

every
 returns 
true
.

With 
reduce

every
 operates on an array and returns a Boolean value, so the accumulator has to be a boolean.

Array.prototype.everyWithReduce = function(tester) {
  return this.reduce((acc, currentElement) =>
    acc && tester(currentElement)
  , true);
}

const array = [1, 2, 3, 4, 5];
const result = array.everyWithReduce(num => num < 10);

// Prints true
console.log(result);

Using 

reduce
, we start with a boolean accumulator value of 
true
 (we’ll discuss the reason later on) and iterate over the array. We then chain the result of the tester callback to the accumulator using the logical AND (
&&
), to eventually return 
true
 if all elements pass the test, and false 
otherwise
.

Why start with 

true
?

If the array is empty, 

every
 returns 
true
 regardless of the test callback (even if the callback returns 
false
).

Else, if all elements fulfill the condition, the chaining of the initial 
true
 value using the logical AND will eventually resolve to 
true
. If not, the chaining will eventually resolve to 
false
.

some

The some() method tests whether at least one element in the array passes the test implemented by the provided function. It returns a Boolean value.

Usage

const array = [1, 2, 3, 4, 5];
const result = array.some(num => num > 3);

// Prints true
console.log(result);

Using a callback function that tests every element in the array, we get a boolean that indicates whether any element passes the test. In this case, the fourth element is larger than 3, and thus 

some
 returns 
true
.

With 
reduce

some
 operates on an array and returns a boolean value, so the accumulator has to be a boolean.

Array.prototype.someWithReduce = function(tester) {
  return this.reduce((acc, currentElement) =>
    acc || tester(currentElement)
  , false);
}

const array = [1, 2, 3, 4, 5];
const result = array.someWithReduce(num => num > 3);

// Prints true
console.log(result);

Using 

reduce
, we start with a boolean accumulator value of 
false
 (we’ll discuss the reason later on) and iterate over the array. We then chain the result of the tester callback to the accumulator using the logical OR (
||
), to eventually return 
true
 if any element passes the test, and 
false
 otherwise.

Why start with 

false
?

If the array is empty, 

some
 returns 
false
 regardless of the test callback (even if the callback returns 
true
).

Else, if any element fulfills the condition, the chaining of the initial 

false
 value using the logical OR will eventually resolve to 
true
. If not, the chaining will eventually resolve to 
false
.

Disclaimer (every and some)

The 

every
 method executes the provided callback function once for each element present in the array until it finds the one where callback returns a falsy value (a value that becomes 
false
 when converted to a boolean). If such an element is found, 
every
 immediately returns 
false
.

Similarly, the 

some
 method executes the callback function once for each element present in the array until it finds the one where callback returns a truthy value (a value that becomes 
true
 when converted to a boolean). If such an element is found, 
some
 immediately returns 
true
.

However, there’s no pretty way to terminate 

reduce
 mid-loop.
This means that while both implementations (both the original 
every/some
 method and the corresponding implementations using 
reduce
) have a runtime of 
O(n)
, the original implementations are likely to terminate without having to iterate over the entire array, making them more efficient.

filter + map

Given an array of numbers, what is the most efficient way to filter out all even elements and square the remaining ones (using ES5 methods)?

Let’s try 

filter
 followed by 
map
:

const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const tester = num => num % 2 === 1;
const transformer = num => num * num;

const result = array.filter(tester).map(transformer);

// Prints [1, 9, 25, 49, 81]
console.log(result);

We create a tester function that keeps only odd elements and a transformer function that squares the given elements. We then use these two callback functions when chaining the 

filter
 and 
map
 methods, and return the desired array.

Let’s use what we know about implementing 

filter
 and 
map
 with 
reduce
, only this time let’s combine them in one go.

const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const tester = num => num % 2 === 1;
const transformer = num => num * num;

const result = array.reduce((newArray, currentElement) => {
  if (tester(currentElement)) {
    const newElement = transformer(currentElement);
    newArray.push(newElement); 
  }
  return newArray;
}, []);

// Prints [1, 9, 25, 49, 81]
console.log(result);

We use the same tester and transformer functions to test if each element should be kept in the array and transform it if it should.

This approach saves us the need to create an intermediate array of filtered values, and we get a slightly more efficient algorithm as we don’t have to iterate over two different arrays (the original and the intermediate).

Conclusion

These were a few examples of how to use the powerful 

reduce
 method to implement other ES5 methods.

How do you use it in your day-to-day coding? Share in the comments!

Sources