Demystifying Array Methods
Bruno Noriller
Posted on July 15, 2023
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
}
- 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
})
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]
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
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]
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
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]
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
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
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
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
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.
Posted on July 15, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.