I have a simple rule of thumb when choosing to use array methods, and I'd like to share it with you. The array methods I'm discussing here are:
arr.filter(y)
arr.map(y)
arr.reduce(y)
arr.forEach(y)
For me, each of these are answers to the question How does an array need to change?
Briefly, the rule of thumb is this:
Use filter
if the array length needs to get smaller;
use map
if the array length stays the same but the
elements of the array need to change in some way; use
reduce
if the array length needs to change, especially
bigger, or the array needs to be transformed into some
other type, like into an object or a string; and use
forEach
only when side-effects are necessary
I use these methods all the time in my code, roughly in that order of priority.
If you're already familiar with these, that's it. That's the blog post. You can stop reading. All the rest is just expansion, reiteration. I should have just stopped there, but what kind of blog post would that be? Absurd, that's what
Or go ahead and skim the examples.
If you're not already familiar with these, this post is probably not the best introduction, but I'll give it a good go. MDN is a great resource to learn more about them.
filter
#
Use filter
if the array length needs to get
smaller
filter
is straight-forward: a filter removes
unwanted stuff, and an array filter removes unwanted elements
from your array. This leads inevitably to the array being the same
length or smaller
filter
takes a predicate for its argument, which
is a function that answers a true or false question
about that input. If the predicate returns true, the element
is included; if it returns false, the element is excluded
from the output array
filter examples #
Consider the array
[1, "tooth", 3, "fort", "fie!",
6]
This is an array of mixed type, containing elements of type
number
and string
. Let's apply a
filter
to remove string
s from the array, so
that it's an array only of number
s
Here is a predicate that returns true
if the input is a
number
and false
otherwise:
(x) => typeof x === "number"
Putting them together, with filter
:
[1, "two", 3, "four", "five",
6].filter((elem) => typeof elem === "number")
This line takes the array
[1, "two", 3, "four", "five",
6]
, applies the filter
(elem) => typeof elem === "number"
to each
element in the array and returns the result, [1,3,6]
, all
and only numbers
Note that the new array is length 3, while the input array is 6. It
was a good decision to apply filter
in this case!
Here's an example from working code:
const searchSpace = dict.filter((entry) => entry.length ==
wordLength);
Here, dict
is literally a list of words
(e.g. "apple", "banana", "pear", ...)
only, humongous
and the predicate,
(entry) => entry.length == wordLength
, returns
true
if the number of characters in entry
is
exactly wordLength
and false
if it's any
other length. Applying this predicate to dict
returns an
array of words that all have a length
matching
wordLength
. The statement assigns all of this to the
constant searchSpace
Marvelous! The new array searchSpace
is smaller than the
input array dict
, consisting entirely of words that are
only of length wordLength
. Another win for the Rule of
Thumb!
map #
Use map
if the array length stays the same but the
elements of the array need to change in some way
The map
method transforms each element of an array into
something else, and returns these new elements in a new array of
exactly the same length. It takes as its argument a function that
accepts an input value and returns another transformed value based on
the input. map
passes each element in turn to this
function, and returns an array with that function applied to each
value
map examples #
Consider the array
[1, "tooth", 3, "fort", "fie!",
6]
and the function
(x) => typeof x === "number"? x : 0
As before, the array is of mixed type, consisting of
number
and string
elements. The function
checks if x
is of type number
and returns
x
unchanged if it is, or 0
if it's of any
other type
Putting them togther with map:
[1, "tooth", 3, "fort", "fie!",
6].map((x) => typeof x === "number"? x : 0)
which returns [1, 0, 3, 0, 0, 6]
Note that the input array length is 6, and the output array is the same length! I am pleased
From working code:
const objToQuery = (obj: {}) =>
Object.entries(obj)
.map((entry) => `${entry[0]}=${entry[1]}`)
.join("&");
(This is a TypeScript function, but just convert this part
(obj: {})
to (obj)
and it's
JavaScript)
This objToQuery
function accepts a simple object (e.g.
{page:1,query:"lime"}
) and returns a string
(e.g. page=1&query=lime
) representation of the
object. This is useful to convert a (simple!) object into a URL query
string
Briefly reviewing Object.entries
(and cribbing from
MDN), it takes an object
and returns an array of its
[key, value]
pairs. So, using our example object,
Object.entries({page:1,query:"lime"})
returns
[["page",1],["query","lime"]]
Applying the line
.map(entry => `${entry[0]}=${entry[1]}`)
to
[["page",1],["query","lime"]]
returns a simple array
["page=1", "query=lime"]
. Finally,
applying .join("&")
returns a string, each
element of the array "joined" with &:
"page=1&query=lime"
Note that the input array for map
,
[["page",1],["query","lime"]]
is of length 2 (2 key-value pairs) and the output array
["page=1", "query=lime"]
is also
length 2 (2 strings). The input and output array stayed the same size,
while each element was transformed! This is exactly as we predicted!!
Astounding
reduce
#
Use reduce
if the array length needs to change,
especially bigger, or the array needs to be transformed
into some other type, like into an object or a string
reduce
is misunderstood and mistrusted, but it's probably
my favorite of the array methods (Here are two devs shamefully making fun of people who use it, but do use it whenever necessary, and use it confidently and with
pride!). I think people are confused by the reduce
name,
which implies that the array is going to get smaller. Not so!
Remember, the array size is going to change or become
not an array. Any time you need to expand an array into a
bigger array or convert it into something else, consider
using reduce
reduce
description
#
Like the other array methods, reduce
takes a function as
an argument. Maybe confusingly, this function is called the
reducer, but the name isn't important. It's just a function
like any other. reduce
, like the other methods, calls
this function once successively for each element of its input array,
in order. Unlike the other methods, this reducer function
itself requires 2 arguments instead of 1. Also unlike the other
methods, it is the second argument of this function that
receives the element value, not the first
The reducer's first argument value is the output of the previous call to the reducer, or if there is no previous element (because it's the first one) it's by default the first element of the array
reduce
returns whatever its reducer returns when
called with the last element of its input array
More info at MDN
Let's have a quick example or two:
reduce
examples
#
Consider [1, "two", 3, "four"]
and
the function (prev, elem) => elem
That function takes two arguments and returns only the second one.
Calling that function with
("a","b")
would just return
"b"
. It's a useless function on its own, but
it's useful as an example.
[1, "two", 3, "four"].reduce((prev, elem) =>
elem)
will call that function successively, and the reducer will receive
values and return them successively, like so:
// the first element of the array is 1
reduce( (1,1) => 1 )
// So, in this iteration, the first argument is
// by default the first element of the array (1),
// The second argument is the element itself,
// and this reducer returns the second argument, the element itself: value 1
reduce( (1,"two") => "two" ) // the first argument is the result of the previous iteration (1),
// but this reducer ignores it, and just returns the second argument
// the second argument is the value of the element of this iteration (value: "two"),
// and so this iteration returns "two",
// which becomes the first argument value for the next iteration:
reduce( ("two",3) => 3 ) // the first argument is the result of the previous iteration ("two"),
// the second argument is the value of the element for this iteration (3),
// and so it returns 3
reduce( (3,"four") => "four" )
// finally, the first argument is the result of the previous iteration (3),
// the second argument is the value of the element for this iteration ("four"),
// and so it returns "four", and since "four" is the last element of the array
// reduce itself returns "four as its final result
So
[1, "two", 3, "four"].reduce((prev, elem) =>
elem)
returns "four"
, which is the value of the last
call to the reducer, which just returns the last element of the array
Note that the size of the input array is 4, and the output isn't an
array at all, but a string
!
array into one thing #
Consider the array
[1, "two", 3, "four", "five",
6]
and the function (str, elem) => `${str}&${elem}
We've seen the array before, an array of mixed types. The function
just takes two arguments and joins them using &
.
Calling this function with "apple"
and
"banana"
will return
"apple&banana"
. That seems rather limited
What happens when we add it to reduce
, as in
[1, "two", 3, "four", "five",
6].reduce((str, elem) => `${str}&${elem}`)
is a bit more interesting, because it returns
"1&two&3&four&five&6"
Congratulations! We reinvented .join
. No wonder those
devs mocked reduce
!
array changes size #
Let's strip duplicated elements out of an array!
Consider the function
(arr,e) => arr.indexOf(e) >= 0? arr : [...arr, e]
This function takes an array as its first argument, arr
,
and any value at all as its second argument e
. It checks
arr.indexOf(e)
to see if e
is in
arr
. If it is
(i.e. arr.indexOf(e) >= 0
) then the function
returns arr
itself. But if the value e
is
not in arr
(i.e. arr.indexOf(e) === -1
) then it returns the
array arr
with e
appended as its final value
(i.e. [...arr, e]
)
So, this function would accept, say ([1,2], 2)
and return
[1,2]
; and would accept ([1,2], 3)
and
return [1,2,3]
. This function when used as a reducer in
reduce
will return another array with all duplicates
stripped
Wait, I forgot to mention one thing before! reduce
can
take 2 arguments! The first is mandatory, and that's the
reducer we've been talking about. The second,
optional argument is the first value to be sent to the
reducer. If it's not supplied, it's by default the first value of the
array, as we discussed before. You okay? Let's take a look:
[1,1,1,2,2,3,3,4,4,4,5,6,6].reduce((arr,e) => arr.indexOf(e)
>= 0? arr : [...arr, e], [])
is going to return [1,2,3,4,5,6]
, which is the original
array stripped of duplicates
Let's step through it
.reducer([],1) // is the first iteration.
// its first argument is `[]`, which we gave as the second argument to reduce, above
// its second argument, 1, is the element value of this iteration
// Reducer checks [] to see if it has `1` in it, and it doesn't, so reducer returns `[1]`
// ...which now becomes the value of the first argument sent to the second iteration:
.reducer([1],1)
// reducer checks `[1]` to see if it contains `1`
// It does! So reducer returns `[1]`
// the next iteration is identical, because arr[3] is value 1
// So skipping ahead to:
.reducer([1],2)
// reducer checks `[1]` to see if it contains `2`
// it does *not*, so reducer returns `[1,2]`
// ... which is the first argument of the next iteration...
// and so on, until the last element of the array:
.reducer([1,2,3,4,5,6],6)
// reducer checks the array to see if 6 is in it, and it is
// so it returns the array itself, unaltered
// and since `reduce` is at the end of the input array
// `reduce` returns the first argument value,
// the array is the solution!
Stick all of that in a named function, and you can strip duplicates out of any array you pass in
const uniq = (arr) => arr.reduce((acc,e) => acc.indexOf(e)
>= 0? acc : [...acc, e], [])
So
uniq([1, 1, 1, 2, 2, 3, 3, 4, 4, 4, 5, 6, 6]); // => [1,2,3,4,5,6]
What else can reduce
do?
#
Oh, lots. Maybe you have a succession of functions and you want to pass the result of one into the next one in turn. For instance, if you want to extract the user id and password sent in an auth header, you could do something like this:
export function getUserIdPassword(headers) {
const headerValue = getAuthHeaderValue(headers);
const decodedHeader = decodeAuthHeader(headerValue);
const parsedHeader = parseAuthHeader(decodedHeader);
return parsedHeader;
}
Or you could do:
export const getUserIdPassword = (headers) =>
[getAuthHeaderValue, decodeAuthHeader, parseAuthHeader].reduce(
(acc, func) => func(acc),
headers
);
Personal preference. No judgment.
Maybe you want to create an object out of key-value pairs. You can do:
const toObj = (keyValues) =>
keyValues.reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {});
toObj([
["page", 1],
["fields", "size,weight,color"],
["priority", "A"],
]); // => {page: 1, fields: "size,weight,color", priority: "A"}
In all of these cases, we're essentially converting an array into
something else, maybe another different array. If you're doing that,
reduce
might get you there faster.
forEach
#
use forEach
only when side-effects are necessary
Briefly, a side effect is when a function changes a value or has an effect outside of its own scope. So, a quick f'rinstance:
let count = 0;
function incr() {
count = count + 1;
}
Calling incr()
causes a side-effect of
incrementing count
by 1
So, if you need to do something like that, forEach
is
your method. The function that forEach
accepts as an
argument returns no value. Nothing. It's of type void
.
So:
(elem) => {}
is pretty much it
Maybe you want to clear a signup form:
const clearSignupForm = () =>
Array.from(getElem("form").querySelectorAll("input")).forEach(
(i) => (i.value = "")
);
Or maybe you want to log array contents to your console:
data.forEach((elem, i) => console.log(i, elem));
Personally, I use forEach
only when a side effect is the
only solution possible. I never do something like:
let keyValObj = {}
[["page",1],["fields","size,weight,color"],["priority","A"]].forEach([key,value] => keyValObj[key] = value)
It becomes difficult to know what's going on with
keyValObj
as the codebase grows. Better to have the
direct assignment:
const keyValObj = [
["page", 1],
["fields", "size,weight,color"],
["priority", "A"],
].reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {});
Extraneous addendium #
I didn't tell you something. All of these array methods take functions as arguments... MDN calls them callbacks so let's do that. All of these array methods take callbacks as we've discussed. The thing I didn't mention is that these array methods pass more values to the callbacks than just the current value of the array. They also receive the current index of the array, and after that, the entire array. That looks like this:
Lets pass into
["zero", "one",
"three"].forEach
this callback:
(elem, index, arr) => console.log({ elem, index, arr })
and see the output.
["zero", "one", "three"].forEach((elem, index, arr) =>
console.log({ elem, index, arr })
);
// {elem: "zero", index: 0, arr: ["zero", "one", "three"]}
// {elem: "one", index: 1, arr: ["zero", "one", "three"]}
// {elem: "three", index: 2, arr: ["zero", "one", "three"]}
As we see here, the elem
value is followed by the
index
value of the current array, followed by
arr
which contains the entire input array
Same goes for the callbacks on all the other methods,
including reduce
. In case you find it useful
Also, you can chain these methods together. Here's an example of how that's done:
exampleEnvEntries = exampleEnv
.replace(/\r/g, "\n")
.replace(/\n{2}/g, "/n")
.split("\n")
.map((l) => l.trim())
.filter((l) => !l.startsWith("#")) // eliminate comments
.filter((l) => l.length > 0); // eliminate blank lines
This takes a string called exampleEnv, splits it into an array which
holds one line of text per element, removes blank spaces at the
beginning and end of each of those lines, filters out any that start
with #
and discards any blank lines
Later, this example file is tested against the user's file and warns the
user if their file is missing something or if they have not changed
something they should have changed, using forEach
:
exampleEnvEntries.forEach((line) => {
const [varName, varValue] = line.split("=");
test(`${varName} is defined as an environmental variable`, () => {
expect(process.env[varName]).toBeDefined();
});
// The value of each SECRET or PASSWORD in 'example.env' is not the same as in process.env
if (varName.indexOf("SECRET") >= 0 || varName.indexOf("PASSWORD") >= 0)
test(`${varName} is not ${varValue}`, () => {
expect(process.env[varName]).not.toBe(varValue);
});
});