JavaScript Array.reduce()

Written by jlowery2663 | Published 2019/04/21
Tech Story Tags: reduce | javascript

TLDRvia the TL;DR App

How do I loathe thee? Let me count the ways…

The Array.reduce() method has been around awhile. Like map(), filter() and find(), it operates upon an array of values by invoking a callback function.

Here’s an example from the developer.mozilla.org site:

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5));
// expected output: 15

Seems straightforward, right? On with the grievances:

0. It doesn’t do what you expect

Let’s look at this again:

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;

// expected: 1 + 2 + 3 + 4 === 10

Now I’ll modify the reducer function to add 1 to each value of the array, then sum it with the previous value. Since I’m adding 1 to four array elements, I expect to get a sum of 14.

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue + 1;

// (1+1) + (2+1) + (3+1) + (4+1) = 2+3+4+5 = 14
console.log(array1.reduce(reducer));

// unexpected output: 13

Hold on… what?

Turns out the first element of the array is the initial accumulator value. So, instead of

(1+1) + (2+1) + (3+1) + (4+1) = 2+3+4+5 = 14

It is actually:

1 + (2+1) + (3+1) + (4+1) = 2+3+4+5 = 13

All those reduce examples online that use a sum function as illustration can mislead you. You can get to the right answer, of course: just add an accumulator initial value as a second argument to the reduce method:

console.log(array1.reduce(reducer, 0));
// desired output: 14

1. It’s misnamed

The name reduce would lead one to believe that it reduces an array of element to a subset of those elements. That’s not really what it does. It can do the opposite of reduce, in fact:

const array1 = [1, 2, 3, 4];  // four elements
const increaser = (accumulator, currentValue) => {
  accumulator.push(currentValue);
  accumulator.push(currentValue*currentValue);
  return accumulator;
};

console.log(array1.reduce(increaser, []));

// output: Array [1, 1, 2, 4, 3, 9, 4, 16] (8 elements)

And it doesn’t even have to return an array:

const array1 = [1, 2, 3, 4];
const transformer = (accumulator, currentValue) => {
  accumulator[currentValue] = currentValue*currentValue;
  return accumulator;
};

console.log(array1.reduce(transformer, {}));
// output: Object { 1: 1, 2: 4, 3: 9, 4: 16 }

2. The order of callback arguments is weird

Let’s compare the callbacks of map(), filter(), find(), and reduce():

map: (currentValue, index, array, this) => {}

filter: (currentValue, index, array, this) => {}

find: (currentValue, index, array, this) => {}

reduce: (accumulator, currentValue, index, array) => {}

Your callback will get the accumulator as the first argument, not the currentValue; also, there is no parameter that takes a this argument.

3. Error: myVar.reduce is not a function

I’ve been spoiled by lodash, which treats both arrays and objects as collections, and collections have map, find, filter, and reduce methods. Not so JavaScript reduce:

var obj1 = {my:"dog", has: "fleas"};
            
const values = (accumulator, currentValue) => accumulator.push(currentValue);
            
console.log(obj1.reduce(values, []));

// > Error: obj1.reduce is not a function

As Dana Carvey used to say, “Nope, not gonna do it. Wouldn’t be prudent.” (Yes, I am that old.)

You can still do something like this, using Object.values or Object.entries:

var obj1 = {my:"dog", has: "fleas"};
            
const values = (acc, [key, value]) => {acc.push(value); return acc}

console.log(Object.entries(obj1).reduce(values, []));

// > Array ["dog", "fleas"]

4. Accumulator sometimes optional; results sometimes random

It’s easy to forget an initial accumulator value, even when you need one. Let’s take that above example, with one modification:

var obj1 = {my:"dog", has: "fleas"};
            
const values = (acc, [key, value]) => {acc.push(value); return acc}

console.log(Object.entries(obj1).reduce(values));

// > Array ["my", "dog", "fleas"]

Did you notice the difference in the code? Hard to spot. What’s worse, you get an array of values back, except the first is invalid. Good thing I have tests someday!

5. Undefined? Huh?

Oh, you forgot to return the accumulator. Silly you. I never make that mistake:

var obj1 = {my:"dog", has: "fleas"};
            
const dog = (acc, [key, value]) => {
  if (key === 'my') {
    acc.push(value);
    return acc;
  }
}

console.log(Object.entries(obj1).reduce(dog, []));

// > undefined

You have to always return the accumulator:

var obj1 = {my:"dog", has: "fleas"};
            
const dog = (acc, [key, value]) => {
  if (key === 'my') {
    acc.push(value);
  }
  return acc;
}

console.log(Object.entries(obj1).reduce(dog, []));

So what do I recommend instead of reduce()?

I am always using Array.reduce(), and you should, too. Just know its quirks. It’s like a tiny chihuahua nipping at your heels that sometimes will get you if you’re not paying attention, but it’s mostly harmless (and far more useful than a chihuahua).

Happy reducing!

<a href="https://medium.com/media/3c851dac986ab6dbb2d1aaa91205a8eb/href">https://medium.com/media/3c851dac986ab6dbb2d1aaa91205a8eb/href</a>


Written by jlowery2663 | I'm an early adopter kinda guy.
Published by HackerNoon on 2019/04/21