What you should know about JavaScript arrays
Our Partner Thomas Lombart shares his knowledge on the subject of Javascript arrays through a detailed article.
Let me make a bold statement: for loops are often useless and make the code hard to read. When it comes to iterating over an array, finding elements, sorting it or whatever you want, there’s probably an array method out there that you can use.
However, some of them are still not that known and used despite their usefulness. I’ll do the hard work for you by giving you the useful methods. Consider this article as your guide to JavaScript arrays methods.
Note: Before starting, you have to know one thing: I’m biased by functional programming. So I tend to use methods that don’t mutate directly the original array. That way, I avoid side effects. I’m not saying you should never mutate an array, but at least know that some methods do it and that it can leads to side-effects. Side-effects can lead to unwanted changes and unwanted changes lead to bugs!
Knowing that, let’s get started.
The essentials
There are four things that you’ll want to know when working with arrays: map
, filter
, reduce
and the spread operator. They are powerful and useful.
map
You’ll use that one a lot. Basically, every time you need to modify the elements of your array, think of using map
.
It takes one parameter: a function that is called on every element of the array. And it returns a new array, so no side effects here.
const numbers = [1, 2, 3, 4]const numbersPlusOne = numbers.map(n => n + 1) // Adds one to every element
console.log(numbersPlusOne) // [2, 3, 4, 5]
You can also create a new array that keeps only one particular property of an object:
const allActivities = [
{ title: 'My activity', coordinates: [50.123, 3.291] },
{ title: 'Another activity', coordinates: [1.238, 4.292] },
// etc.
]const allCoordinates = allActivities.map(activity => activity.coordinates)
console.log(allCoordinates) // [[50.123, 3.291], [1.238, 4.292]]
So, remember, whenever you need to transform an array, think of using map.
filter
The name of this method is pretty explicit here: use it when you want to filter an array.
Just like map
does, it takes a function as its only parameter that is called on every element of the array. This function needs to return a boolean:
true
if you want to keep the element in the arrayfalse
if you don’t want to keep it.
Then you’ll have a shiny new array with the elements you wanted to keep.
For example, you can keep just the odd numbers in an array:
const numbers = [1, 2, 3, 4, 5, 6]
const oddNumbers = numbers.filter(n => n % 2 !== 0)
console.log(oddNumbers) // [1, 3, 5]
Or you can also use it to remove a particular item in an array:
const participants = [
{ id: 'a3f47', username: 'john' },
{ id: 'fek28', username: 'mary' },
{ id: 'n3j44', username: 'sam' },
]function removeParticipant(participants, id) {
return participants.filter(participant => participant.id !== id)
}console.log(removeParticipant(participants, 'a3f47')) // [{ id: 'fek28', username: 'mary' }, { id: 'n3j44', username: 'sam' }];
reduce
The most difficult method to understand in my opinion. But once you master it, it’s crazy how many things you can do with it.
Basically, reduce
is about taking an array of values and combine them into one value. It takes two parameters, a callback function which is our reducer and an optional initial value (which is the first item of the array by default). The reducer itself takes four parameters:
- The accumulator: it accumulates the returned values in your reducer.
- The current value of the array
- The current index
- The array
reduce
was called upon
Most of the time, you’ll just use the first two parameters: the accumulator and the current value.
Let’s not be too theoretical. Here is the most common example of reduce
:
const numbers = [37, 12, 28, 4, 9]
const total = numbers.reduce((total, n) => total + n)
console.log(total) // 90
On the first iteration, the accumulator, which is total
, takes an initial value of 37. The returned value is 37 + n
and n
is equal to 12, thus 49. On the second iteration, the the accumulator is equal to 49, the returned value is 49 + 28 = 77. And so on and so forth.
reduce
is so powerful that you can actually use it to build a lot of array methods like map
or filter
:
const map = (arr, fn) => {
return arr.reduce((mappedArr, element) => {
return [...mappedArr, fn(element)]
}, [])
}console.log(map([1, 2, 3, 4], n => n + 1)) // [2, 3, 4, 5]const filter = (arr, fn) => {
return arr.reduce((filteredArr, element) => {
return fn(element) ? [...filteredArr] : [...filteredArr, element]
}, [])
}console.log(filter([1, 2, 3, 4, 5, 6], n => n % 2 === 0)) // [1, 3, 5]
Basically, we gave reduce
an initial value of []
: our accumulator. For map
, we ran a function whose result is added at then end of the accumulator thanks to the spread operator (we’ll see it just after, don’t worry.). For filter
, it’s nearly the same except that we ran the filter function on the element. If it returns true, we return the previous array, otherwise we add the element to the end of the array.
Let’s see a more advanced example: deeply flatten an array, that is to say transforming something like [1, 2, 3, [4, [[[5, [6, 7]]]], 8]]
into [1, 2, 3, 4, 5, 6, 7, 8]
.
function flatDeep(arr) {
return arr.reduce((flattenArray, element) => {
return Array.isArray(element)
? [...flattenArray, ...flatDeep(element)]
: [...flattenArray, element]
}, [])
}console.log(flatDeep([1, 2, 3, [4, [[[5, [6, 7]]]], 8]])) // [1, 2, 3, 4, 5, 6, 7, 8]
This example is similar to map
except that here we make use of recursion. I won’t explain it because it’s outside the scope of this article.
Spread operator (ES2015)
I agree, this is not a method. However, using the spread operator can help you achieve many things when working with arrays. In fact, you can use it to expand the values of an array in another array. From that point, you can make a copy of an array or concatenate multiple arrays.
const numbers = [1, 2, 3]
const numbersCopy = [...numbers]
console.log(numbersCopy) // [1, 2, 3]const otherNumbers = [4, 5, 6]
const numbersConcatenated = [...numbers, ...otherNumbers]
console.log(numbersConcatenated) // [1, 2, 3, 4, 5, 6]
Caution: the spread operator does a shallow copy of the original array. But what does shallow mean? 🤔
Well, a shallow copy will duplicate the original elements as little as possible. So when you have an array containing numbers, strings or booleans (primitive types), there’s no problem, values are really duplicated. However, this is not the same for objects or arrays. Only the reference to the original value will be copied! Therefore, if you make a shallow copy of an array containing an object and that you modify the object in the copied array, it will also modify the object in the original array since they have the same reference.
const arr = ['foo', 42, { name: 'Thomas' }]
let copy = [...arr]copy[0] = 'bar'console.log(arr) // No mutations: ["foo", 42, { name: "Thomas" }]
console.log(copy) // ["bar", 42, { name: "Thomas" }]copy[2].name = 'Hello'console.log(arr) // /!\ MUTATION ["foo", 42, { name: "Hello" }]
console.log(copy) // ["bar", 42, { name: "Hello" }]
So, if you want to make a ‘real’ copy of an array that contains object or arrays, you can use a lodash function like cloneDeep. But don’t feel like you have to do such a thing. The goal here is to be aware of how things work under the hood.
Good to know
You’ll find below other methods that are good to know and that can help you with some problems such as searching an element in an array, taking a portion of an array and more.
includes (ES2015)
Have you ever used indexOf
to know if something is in an array or not? Awful way to do it right? Luckily for us, includes
does that for us. Give a parameter to includes
and it will search in the array if the element exists.
const sports = ['football', 'archery', 'judo']
const hasFootball = sports.includes('football')
console.log(hasFootball) // true
concat
The concat method can be used to merge two or more arrays.
const numbers = [1, 2, 3]
const otherNumbers = [4, 5, 6]const numbersConcatenated = numbers.concat(otherNumbers)
console.log(numbersConcatenated) // [1, 2, 3, 4, 5, 6]// You can merge as many arrays as you want
function concatAll(arr, ...arrays) {
return arr.concat(...arrays)
}console.log(concatAll([1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12])) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
forEach
Whenever you want to execute something for each array element, you’ll want to use forEach
. It takes a function as a parameter that takes itself three parameters: the current value, the index and the array:
const numbers = [1, 2, 3, 4, 5]
numbers.forEach(console.log)
// 1 0 [ 1, 2, 3 ]
// 2 1 [ 1, 2, 3 ]
// 3 2 [ 1, 2, 3 ]
indexOf
It is used to return the first index at which a given element can be found in the array. indexOf
was also widely used to check whether an element is in an array
or not. To be honest, I don’t use it that much as of today.
const sports = ['football', 'archery', 'judo']const judoIndex = sports.indexOf('judo')
console.log(judoIndex) // 2
find
The find
method is quite similar to the filter
method. You have to provide it a function that test each array’s element. However, find
stops testing elements as soon as it finds one that passes the test. Not filter
. filter
will iterate over the whole array no matter what.
const users = [
{ id: 'af35', name: 'john' },
{ id: '6gbe', name: 'mary' },
{ id: '932j', name: 'gary' },
]const user = users.find(user => user.id === '6gbe')
console.log(user) // { id: '6gbe', name: 'mary' }
So use filter
when you want to… well filter your whole array. Use find
when you’re sure you’re searching for a unique element in your array.
findIndex
It’s exactly the same as the find
method except that it returns the index of the first element found instead of the element directly.
const users = [
{ id: 'af35', name: 'john' },
{ id: '6gbe', name: 'mary' },
{ id: '932j', name: 'gary' },
]const user = users.findIndex(user => user.id === '6gbe')
console.log(user) // 1
You may think that findIndex
is the same as indexOf
. Well… not exactly. The first parameter of indexOf
is a primitive value (boolean, number, string, null, undefined or a symbol) while the first parameter of findIndex
is a callback function.
So when you need to search the index of an element in an array of primitive values, you can use indexOf
. If you have more complex elements such as objects, use findIndex
.
slice
Whenever you need to take a portion of an array or copy an array, you can use slice
. But be careful, just like the spread operator, slice
returns a shallow copy of that portion!
const numbers = [1, 2, 3, 4, 5]
const copy = numbers.slice()
I said at the beginning of an article that for loops were often useless. Let me give you an example of how you can get rid of one.
Let’s say you want to retrieve a certain amount of chat messages from an API and you want to display only five of them. You’ll find below two approches: one with a for loop the other with slice
.
// The "traditional way" to do it:
// Determine the number of messages to take and use a for loop
const nbMessages = messages.length < 5 ? messages.length : 5
let messagesToShow = []
for (let i = 0; i < nbMessages; i++) {
messagesToShow.push(posts[i])
}// Even if "arr" has less than 5 elements,
// slice will return an entire shallow copy of the original array
const messagesToShow = messages.slice(0, 5)
some
If you want to test that at least one element of an array passes a test, well you can use some
. Just like map
, filter
or find
, some
takes a callback function as its only parameter. It returns true
if at least one element pass the test, false
otherwise.
You can use some
when you are dealing with permissions for example:
const users = [
{
id: 'fe34',
permissions: ['read', 'write'],
},
{
id: 'a198',
permissions: [],
},
{
id: '18aa',
permissions: ['delete', 'read', 'write'],
},
]const hasDeletePermission = users.some(user =>
user.permissions.includes('delete')
)
console.log(hasDeletePermission) // true
every
Similar to some
except that every
tests if all elements pass the condition (instead of at least one).
const users = [
{
id: 'fe34',
permissions: ['read', 'write'],
},
{
id: 'a198',
permissions: [],
},
{
id: '18aa',
permissions: ['delete', 'read', 'write'],
},
]const hasAllReadPermission = users.every(user =>
user.permissions.includes('read')
)
console.log(hasAllReadPermission) // false
flat (ES2019)
These are the brand new methods that are coming in the JavaScript world. Basically, flat
creates a new array by concatenating all sub-array elements into it. It accepts one parameter, a number, which represents how deep you want to flatten your array:
const numbers = [1, 2, [3, 4, [5, [6, 7]], [[[[8]]]]]]const numbersflattenOnce = numbers.flat()
console.log(numbersflattenOnce) // [1, 2, 3, 4, Array[2], Array[1]]const numbersflattenTwice = numbers.flat(2)
console.log(numbersflattenTwice) // [1, 2, 3, 4, 5, Array[2], Array[1]]const numbersFlattenInfinity = numbers.flat(Infinity)
console.log(numbersFlattenInfinity) // [1, 2, 3, 4, 5, 6, 7, 8]
flatMap (ES2019)
Can you guess what this method does? I bet you can just with the name.
First it runs a mapping function on each element. Then it flattens the array once. Easy peasy!
const sentences = [
'This is a sentence',
'This is another sentence',
"I can't find any original phrases",
]const allWords = sentences.flatMap(sentence => sentence.split(' '))
console.log(allWords) // ["This", "is", "a", "sentence", "This", "is", "another", "sentence", "I", "can't", "find", "any", "original", "phrases"]
In this example, you have many sentences in an array and you want to get all the words. Instead of using map
to split all the sentences into words and then flatten the array, you can directly use flatMap
.
Nothing to do with flatMap
, but you can then count the number of words with the reduce
function (just to show you another use-case of reduce
🙂)
const wordsCount = allWords.reduce((count, word) => {
count[word] = count[word] ? count[word] + 1 : 1
return count
}, {})
console.log(wordsCount) // { This: 2, is: 2, a: 1, sentence: 2, another: 1, I: 1, "can't": 1, find: 1, any: 1, original: 1, phrases: 1, }
flatMap
is also often used in Reactive Programming, you can see an example of it here.
join
If you need to create a string based on the array’s elements, you’re looking for join
. It allows to create a new string by concatenating all the array’s elements, separated by a provided separator.
For example, you can display at a glance all the participants of an activity using join
:
const participants = ['john', 'mary', 'gary']
const participantsFormatted = participants.join(', ')
console.log(participantsFormatted) // john, mary, gary
Here is a more real-word example where you may want to filter the participants before and get their name:
const potentialParticipants = [
{ id: 'k38i', name: 'john', age: 17 },
{ id: 'baf3', name: 'mary', age: 13 },
{ id: 'a111', name: 'gary', age: 24 },
{ id: 'fx34', name: 'emma', age: 34 },
]const participantsFormatted = potentialParticipants
.filter(user => user.age > 18)
.map(user => user.name)
.join(', ')console.log(participantsFormatted) // gary, emma
from
This is a static method that creates a new Array from an array-like or iterable object like a string for example. It can be useful when you’re working with the dom.
const nodes = document.querySelectorAll('.todo-item') // this is an instance of NodeList
const todoItems = Array.from(nodes) // now, you can use map, filter, etc. as you're workin with an array!
Have you seen that we used Array
instead of an array instance? That’s why from
is called a static method.
Then you can have fun with these nodes and for example registering an event listener on each one of them with forEach
:
todoItems.forEach(item => {
item.addEventListener('click', function() {
alert(`You clicked on ${item.innerHTML}`)
})
})