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)
})
})