Array methods summarised
Intro
JavaScript Arrays have lots of built in methods on their prototype. Some of them mutate - ie, they change the underlying array in-place. Luckily, most of them do not - they instead return an entirely distinct array. Since arrays are conceptually a contiguous list of items, it helps code clarity and maintainability a lot to be able to operate on them in a “functional” way. (I’ll also insist on referring to an array as a “list” - although in some languages, List is a native data type, in JS and this post, I’m referring to the concept. Everywhere I use the word “list” you can assume I’m talking about a JS Array) This means, to perform a single operation on the list as a whole (“atomically”), and to return a new list - thus making it much simpler to think about both the old list and the new one, what they contain, and what happened during the operation.
Below are some of the methods that iterate - in other words, that operate on the entire list, one item at a time. When you call them, you provide a callback function - a single function that expects to operate on one item at a time. Based on the Array method you’ve chosen, the callback gets specific arguments, and may be expected to return a certain kind of value - and (except for forEach) the return value determines the final return value of the overarching array operation. Although most of the methods are guaranteed to execute for each item in the array - for all of them - some of the methods can stop iterating partway through; when applicable, this is indicated below.
All array methods iterate in what is traditionally called “left to right” - more accurately (and less ethnocentrically) from index 0, to index length - 1 - also called “start” to “end”. reduceRight is an exception in that it iterates in reverse - from end to start.
forEach
- callback answers: here’s an item. do something nutty with it, i don’t care what.
- callback gets these arguments:
item,index,list - final return value: nothing - in other words,
undefined - example use case:
;[1, 2, 3].forEach((item, index) => {
console.log(item, index)
})
map
- callback answers: here’s an item. what should i put in the new list in its place?
- callback gets these arguments:
item,index,list - final return value: list of new items
- example use case:
const three = [1, 2, 3]
const doubled = three.map((item) => {
return item * 2
})
console.log(three === doubled, doubled) // false, [2, 4, 6]
filter
- callback is a predicate - it should return a truthy or falsy value
- callback answers: should i keep this item?
- callback gets these arguments:
item,index,list - final return value: list of kept items
- example use case:
const ints = [1, 2, 3]
const evens = ints.filter((item) => {
return item % 2 === 0
})
console.log(ints === evens, evens) // false, [2]
reduce
- callback answers: here’s the result from the previous iteration. what should i pass to the next iteration?
- callback gets these arguments:
result,item,index,list - final return value: result of last iteration
- example use case:
// NOTE: `reduce` and `reduceRight` take an optional "initialValue" argument, after the reducer callback.
// if omitted, it will default to the first item.
const sum = [1, 2, 3].reduce((result, item) => {
return result + item
}, 0) // if the `0` is omitted, `1` will be the first `result`, and `2` will be the first `item`
reduceRight: (same as reduce, but in reversed order: last-to-first)
some
- callback is a predicate - it should return a truthy or falsy value
- callback answers: does this item meet your criteria?
- callback gets these arguments:
item,index,list - final return value:
trueafter the first item that meets your criteria, elsefalse - note: stops iterating once it receives a truthy value from your callback.
- example use case:
const hasNegativeNumbers = [1, 2, 3, -1, 4].some((item) => {
return item < 0
})
console.log(hasNegativeNumbers) // true
every
- callback is a predicate - it should return a truthy or falsy value
- callback answers: does this item meet your criteria?
- callback gets these arguments:
item,index,list - final return value:
falseafter the first item that failed to meet your criteria, elsetrue - note: stops iterating once it receives a falsy value from your callback.
- example use case:
const allPositiveNumbers = [1, 2, 3].every((item) => {
return item > 0
})
console.log(allPositiveNumbers) // true
find
- callback is a predicate - it should return a truthy or falsy value
- callback answers: is this item what you’re looking for?
- callback gets these arguments:
item,index,list - final return value: the item you’re looking for, or undefined
- note: stops iterating once it receives a truthy value from your callback.
- example use case:
const objects = [{ id: 'a' }, { id: 'b' }, { id: 'c' }]
const found = objects.find((item) => {
return item.id === 'b'
})
console.log(found === objects[1]) // true
findIndex
- callback is a predicate - it should return a truthy or falsy value
- callback answers: is this item what you’re looking for?
- callback gets these arguments:
item,index,list - final return value: the index of the item you’re looking for, or
-1 - note: stops iterating once it receives a truthy value from your callback.
- example use case:
const objects = [{ id: 'a' }, { id: 'b' }, { id: 'c' }]
const foundIndex = objects.findIndex((item) => {
return item.id === 'b'
})
console.log(foundIndex === 1) // true
findLast
- callback is a predicate - it should return a truthy or falsy value
- callback answers: is this item what you’re looking for? (searching from the end)
- callback gets these arguments:
item,index,list - final return value: the last item that matches, or
undefined - note: iterates from
endtostart, and stops once it receives a truthy value from your callback. - example use case:
const numbers = [1, 2, 3, 4, 5]
const lastEven = numbers.findLast((item) => {
return item % 2 === 0
})
console.log(lastEven) // 4
findLastIndex
- Same as
findLast, but returns the index of the last matching item, or-1.
const numbers = [1, 2, 3, 4, 5]
const lastEvenIndex = numbers.findLastIndex((item) => {
return item % 2 === 0
})
console.log(lastEvenIndex) // 3
flatMap
- callback answers: here’s an item. what should i put in the new list in its place? (and if you return a list, i’ll flatten it one level)
- callback gets these arguments:
item,index,list - final return value: new list, flattened by one level
- note: equivalent to
.map(...).flat(), but in a single pass. Great for “map, but sometimes zero or many results per item”. - example use case:
const sentences = ['hello world', 'foo bar']
const words = sentences.flatMap((item) => {
return item.split(' ')
})
console.log(words) // ['hello', 'world', 'foo', 'bar']
sort
- callback is a comparator - it should return either a number either < 0, 0, or > 0
- callback answers: how do the two items compare with each other
- callback gets these arguments:
oneElement,theOtherElement - final return value:
number < 0, ifoneElementshould preceedtheOtherElement,0to keep the relative order,> 0to placeoneElementat a later index thantheOtherElement - note:
sortmutates the array in-place (and returns the same array). If you don’t want that, usetoSortedbelow. - example use case:
const objects = ['John', 'Doe', 'Foo', 'Bar']
const sortedObjects = objects.sort((one, two) => -one.localeCompare(two)) // reverses the string in reverse order
console.log(sortedObjects) // ['John', 'Foo', 'Doe', 'Bar']
Compare function examples
export const compare = (a, b):number => {
if (a is less than b by some ordering criterion) {
return -1;
}
if (a is greater than b by the ordering criterion) {
return 1;
}
// a must be equal to b
return 0;
}
export const compareNumbers = (a:number, b:number) => {
return a - b;
}
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export const sortByDisplayOrder = (a: any, b: any): number => {
return a.displayOrder - b.displayOrder
}
// Sort by property priority
export const sortByPropertyPriority = <T>(
key: string,
sortingOrder: Record<string, number>,
order: 'asc' | 'desc' = 'asc',
) => {
return (a: T, b: T): number => {
// eslint-disable-next-line
// @ts-ignore
// eslint-disable-next-line no-prototype-builtins
if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) return 0
const first =
a[key].toLowerCase() in sortingOrder
? sortingOrder[a[key]]
: Number.MAX_SAFE_INTEGER
const second =
b[key].toLowerCase() in sortingOrder
? sortingOrder[b[key]]
: Number.MAX_SAFE_INTEGER
let result = 0
if (first < second) {
result = -1
} else if (first > second) {
result = 1
}
return order === 'desc' ? ~result : result
}
}
Change-by-copy methods (ES2023)
For years, a handful of array methods (sort, reverse, splice) broke the “return a new list” rule and mutated the array in-place - forcing you into the [...array].sort() dance. ES2023 finally fixed this with non-mutating counterparts. Each one returns a new array and leaves the original untouched.
toSorted
- Non-mutating version of
sort. Takes the same optional comparator callback. - final return value: a new, sorted list
- example use case:
const names = ['John', 'Doe', 'Foo', 'Bar']
const sorted = names.toSorted()
console.log(sorted) // ['Bar', 'Doe', 'Foo', 'John']
console.log(names) // ['John', 'Doe', 'Foo', 'Bar'] - untouched!
toReversed
- Non-mutating version of
reverse. No callback. - final return value: a new list in reverse order
- example use case:
const numbers = [1, 2, 3]
const reversed = numbers.toReversed()
console.log(reversed) // [3, 2, 1]
console.log(numbers) // [1, 2, 3]
toSpliced
- Non-mutating version of
splice. Same arguments:start,deleteCount, then any items to insert. - final return value: a new list with items removed and/or inserted
- note: unlike
splice(which returns the removed items),toSplicedreturns the new modified list. - example use case:
const months = ['Jan', 'Mar', 'Apr']
const fixed = months.toSpliced(1, 0, 'Feb')
console.log(fixed) // ['Jan', 'Feb', 'Mar', 'Apr']
console.log(months) // ['Jan', 'Mar', 'Apr']
with
- Non-mutating version of bracket assignment (
array[index] = value). Takes anindex(negative counts from the end) and avalue. - final return value: a new list with the item at
indexreplaced byvalue - example use case:
const numbers = [1, 2, 3]
const replaced = numbers.with(1, 99)
console.log(replaced) // [1, 99, 3]
console.log(numbers) // [1, 2, 3]
// negative indices work too
console.log(numbers.with(-1, 0)) // [1, 2, 0]
Other useful newer methods
at
- Like bracket access, but accepts negative indices -
at(-1)is the last item. So much nicer thanarray[array.length - 1].
const numbers = [1, 2, 3]
console.log(numbers.at(-1)) // 3
console.log(numbers.at(0)) // 1
flat
- Flattens nested arrays by a given depth (default
1). PassInfinityto flatten fully.
const nested = [1, [2, [3, [4]]]]
console.log(nested.flat()) // [1, 2, [3, [4]]]
console.log(nested.flat(Infinity)) // [1, 2, 3, 4]
includes
- Returns
trueif the array contains the given value. Simpler thanindexOf(x) !== -1, and unlikeindexOfit can findNaN.
console.log([1, 2, 3].includes(2)) // true
console.log([Number.NaN].includes(Number.NaN)) // true
console.log([Number.NaN].indexOf(Number.NaN)) // -1 🙃
Object.groupBy
- Not an array method (it’s a static method on
Object), but it iterates a list just like the methods above. Groups items into an object keyed by whatever your callback returns. - callback answers: which group does this item belong to?
- callback gets these arguments:
item,index
const inventory = [
{ name: 'asparagus', type: 'vegetable' },
{ name: 'banana', type: 'fruit' },
{ name: 'cherry', type: 'fruit' },
]
const grouped = Object.groupBy(inventory, (item) => item.type)
// {
// vegetable: [{ name: 'asparagus', ... }],
// fruit: [{ name: 'banana', ... }, { name: 'cherry', ... }],
// }