Demystifying Array Methods

Bruno Noriller - Jul 15 '23 - - Dev Community

Someway, somehow, people think the for loops are easier to grasp and understand… I’m here to change that.

I’ll be using Javascript, but most languages implement the methods in one way or another, sometimes with some variance in naming.

Basics

Most methods, with exceptions, have the same signature.

Let’s compare with the for loops:

const array = ['a', 'b', 'c'];

// the classic `for` loop you can have better
// control over the indexed you're using
for (let index = 0; index < array.length; index++) {
  const element = array[index];
  console.log(element); // a, b, c
}

// the `for in` gives the index
for (const index in array) {
  const element = array[index];
  console.log(element); // a, b, c
}

// the `for of` gives the element
for (const element of array) {
  console.log(element); // a, b, c
}
Enter fullscreen mode Exit fullscreen mode
  • element - usually this is all you need, really
  • index - there are uses to using the index
  • array - when chaining methods, this allows you to use the current values in the step you’re in.

Two things to remember are:

  • Use the best method for what you’re doing

If you don’t need to return anything, using a map that returns will make me think you forgot to return something.

Likewise, there are many roundabout ways that the current state of the language has better ways of doing, like using findIndex !== -1 when a some will do the same.

  • You can chain methods

As long as what you return is another array, then you can just do something and pass it to the next step.

Sometimes this makes sense, but sometimes, you may want to assign them to descriptive variables.

forEach

“For each” element in the array, it does something and doesn’t return anything.

['a', 'b', 'c'].forEach((element) => {
  // console.log doesn't return anything
  // so it's perfect for the forEach
  console.log(element); // a, b, c
})
Enter fullscreen mode Exit fullscreen mode

You use it when you just need to do something and not return anything: logs, toasts…

It’s easy to understand and use, but since it doesn’t return anything, there aren’t really many uses. (there are better methods for whatever you’re thinking)

map

The poster child of the array methods!

For each element in the array, it returns a value.

(note: even if you don’t return, it returns undefined)

// not adding {} in the arrow function, means it returns implicitly
[1, 2, 3].map(element => element * 2); // result array is [2, 4, 6]

[1, 2, 3].map(element => {
  if(element % 2 !== 0) { // remainder of the division by 2 not equal to zero 
    return element * 2;
  }
  // no "else" returns
}) // resulting array [2, undefined, 6]
Enter fullscreen mode Exit fullscreen mode

Since it returns an array of the same size, it can be used for multiple things and makes chaining easy.

reduce

The bane of people learning the methods… but it’s not really that complicated.

For each element in the array, it returns “one” thing (that can be anything).

Optionally, you can have a starting value.

// classical use case
[1, 2, 3].reduce((accumulator, element) => accumulator + element, 0); // returns 6

// classical mistake nr 1: not returning anything
[1, 2, 3].reduce((accumulator, element) => {
  console.log(accumulator); // 0 (initial value), undefined, undefined (undefined because you didn't return)
  console.log(element); // 1, 2, 3
  accumulator + element; // this would be 1 (0 + 1), then NaN, NaN (number + undefined/NaN)
  // always remember to return something, even if only the accumulator
}, 0);

// (possible) mistake: not having an initial value
// no initial value means the first element is the initial value
[1, 2, 3].reduce((accumulator, element)=> accumulator + element); // retuns 6
[].reduce((accumulator, element)=> accumulator + element); // throws because empty array with no initial value

// things start to be different for more complex use cases
[1, 2, 3].reduce((accumulator, element)=>{
  if (element % 2 === 0) {
    accumulator.even.push(element);
  } else {
    accumulator.odd.push(element);
  }

  return accumulator;
// below I'm using JSDoc to type the initial value
// because using JS doesn't mean not using types ;]
}, /** @type {{ even: number[], odd: number[] }} */ ({ even: [], odd: []}));
// this returns: { even: [ 2 ], odd: [ 1, 3 ] }
// Not using an initial value will break this in multiple ways.

// this is another way to write the same thing
[1, 2, 3].reduce((accumulator, element)=>{
  if (element % 2 === 0) {
    return {
      ...accumulator,
      even: accumulator.even.concat(element),
    }
  }

  return {
    ...accumulator,
    odd: accumulator.odd.concat(element),
  }
}, /** @type {{ even: number[], odd: number[] }} */({ even: [], odd: []}));
// the important thing to remember is to always return something

// the "one" thing it returns can be anything:
[1, 2, 3].reduce((accumulator, element) => {
  if (element % 2 === 0) {
    accumulator[1] += element;
  } else {
    accumulator[0] += element;
  }
  return accumulator;
}, /** @type {[number, number]} [*/([0, 0]));
// this returns [4, 2]
// and would again have multiple problems without an initial value
Enter fullscreen mode Exit fullscreen mode

If you were to instantiate one or more variables outside, then use a for loop or even a forEach and mutate it, then it’s a use case for reduce.

It always return “one” thing, and as you can see it can be anything: number, string, array, object…

While, you can do basically anything with reduce, check if there isn’t any other method that cover the case better.

There’s also a reduceRight that is the same thing, but starts from the last element.

filter

For each element in the array, returns an array that can be of a smaller size.

While reduce returns “one” thing, filter always return an array that can be from empty to the same size of the input.

[1, 2, 3].filter(element => element % 2 !== 0); // returns [1, 3]
[1, 2, 3].filter(element => element % 5 === 0); // return [0]

// one common use case: remove falsy values
[1, 2, 3].map(element => {
  if (element % 2 !== 0) {
    return element;
  }
}) // at this point you would have: [1, undefined, 3]
  .filter(Boolean); // after this return [1, 3]
Enter fullscreen mode Exit fullscreen mode

If you want to remove values from an array, filter is the method you want.

And for those who didn’t know: filter(Boolean) is an elegant way of removing falsy values.

You should be careful, that it removes falsy values like '' (empty string) and 0 (zero).

flat and flatMap

Flatting an array is not something you use every day, but is certainly useful in many cases. Basically, to flat is to remove dimensions from an array.

If you have a 2x2 matrix: [[1, 1], [1, 1]] and you flat it, you end up with a simple 4 items array: [1, 1, 1, 1].

[1, 2, 3].map(element => {
  return [[[element]]]; // sometimes you return values/tuples
  // depending on what you really wanted to return, you just need to flat it
}) // here would return [[[[1]]],[[[2]]],[[[3]]]]
  .flat(1) // returns [[[1]],[[2]],[[3]]]
  .flat(2); // returns [1, 2, 3]

[1, 2, 3].map(element => {
  return [[[element]]];
}) // here would return [[[[1]]],[[[2]]],[[[3]]]]
  .flat(Infinity); // returns [1, 2, 3]
  // flatting to infinity means it will remove all dimensions
Enter fullscreen mode Exit fullscreen mode

You call flat from an array and it takes a number that is how many dimensions you’re stripping from it. If you want a simple array as result no matter how many dimensions the array has, then use the flat(Infinity) as it will do just that.

[1, 2, 3].flatMap(element => {
  return [element * 2]; // returns [2], [4], [6]
}); // returns [2, 4, 6] because it flattened

[1, 2, 3].flatMap(element => {
  if (element % 2 === 0) {
    return element * 2; // return 4
  }
  // without returning anything, it would result in [undefined, 4, undefined]
  // while flatMap doesn't remove falsy values
  // a flattened empty array disappear
  return [];
}); // returns [4]
Enter fullscreen mode Exit fullscreen mode

flatMap is basically a map followed by flat(1).

TIL: you can use flatMap and return [] for falsy values. It will return just the values

some and every

Those are fun ones that, after learning about you will find use cases everywhere!

Usually, the use case is either: filter(/* for something */).length /* (not) equal to something */ or Boolean(find(/* something (not) equal */)) .

For each element in the array, some returns true if at least one passes the predicate, every if all passes it.

[1, 2, 3].some(element => element % 2 === 0); // true
[1, 2, 3].every(element => element % 2 === 0); // false
Enter fullscreen mode Exit fullscreen mode

One cool thing is that they both return early, some on the first truthy value returned and every on the first falsy value returned.

When all you need is a boolean from if there is or not something, then use either some or every.

find and findIndex

find will return the first element that passes the predicate or undefined otherwise, findIndex will return the index of the element or -1 otherwise.

Both start searching from index 0, but if you want the last of those, then use the “last” variations.

// returns the element:
[1, 2, 3].find(element => element % 2 !== 0); // 1
[1, 2, 3].findLast(element => element % 2 !== 0); // 3

// return the index:
[1, 2, 3].findIndex(element => element % 2 !== 0); // 0
[1, 2, 3].findLastIndex(element => element % 2 !== 0); // 2
Enter fullscreen mode Exit fullscreen mode

includes

Sometimes you just want to check if a value exists inside an array, this checks for deep equality.

[1, 2, 3].includes(1); // true
[1, 2, 3].includes('1'); // false
Enter fullscreen mode Exit fullscreen mode

Likely, we can include here indexOf and lastIndexOf both checking for a value, but returning the index or -1 otherwise.

[1, 0, 1].indexOf(1); // 0
[1, 0, 1].indexOf('1'); // -1

[1, 0, 1].lastIndexOf(1); // 2
[1, 0, 1].lastIndexOf('1'); // -1
Enter fullscreen mode Exit fullscreen mode

The use case for includes is usually searching an array of primitive values (string, number, symbols…), meanwhile some and every are used for things more complex than if the value is there or not or for objects.

Last remarks

The MDN docs are as good as they come. You might not have known all of those existed, but once you do they are pretty straightforward.

Then again, some people might know the methods exist, but not how to apply them. So, if you want, send examples you don’t know how to apply the methods and I can refactor options.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .