Reviewing JavaScript’s Key Array Operators

Andrew Crites
ITNEXT
Published in
13 min readMar 29, 2019

--

A Solar Array

As I’m a TypeScript guy, this article will be written with a TypeScript focus, but the code should generally work with the latest versions of JavaScript too.

There are a lot of Array operators in JavaScript. There may be more than you think. I looked at the list, and there were more than I thought. New ones are added often as well, so it might be a good idea to familiarize yourself with array operators (or methods) with some regularity.

This article will be a review of what I consider to be the core JavaScript array operators. This is just my opinion, and some of the operators I leave off here may be more valuable to some people, but these are Array operators that I find people are often not aware of even though they are very functionally and semantically powerful.

I’ll split these into categories that make sense to me. Then, for each operator I will provide:

  • An explanation of its purpose
  • Whether it modifies the array in place
  • A working code example on https://stackblitz.com (that you can play around with if you like!)

Note that arrays are 0 indexed, but I’ll be referring to the element you would get from array[0] as the first element and array[1] as the second. I will not refer to a 0th element.

Iteration

For the unfamiliar, iteration is the process of moving over a collection of items. My map vs for loop article has more specifics on what iteration is and how it works. For our purposes, these are operators that run a callback function, or predicate, over each element of the array.

The predicate for iterators typically takes three arguments:

  • The array element for the current round of iteration
  • The index of this array element
  • The entire array that you called the operator on

Some predicates take more arguments than this, but don’t worry. I’ll specify this if it comes up.

map

The map operator iterates over an array and applies or projects the predicate over each element of the array and returns a new array of the same length composed of the return value of that predicate function for each element.

map returns a new array, so you can get its result without modifying the original array.

You will often use map to get a representation of your original data in some new format. For example, you might have a list of names that you get from a database. You may want to display these names in all caps, but you don’t know how they will be stored originally. You can use map to get the all caps representation of each element.

It’s also very common to have an array of objects and to want to get a specific property of each object. This is often called “plucking,” and some libraries have a pluck operator.

Keep in mind that your predicate function passed to map should probably return something. If you’re not returning anything, you might want to use forEach or something else instead, but see my map vs. for loop article for more details about avoiding that sort of thing.

Finally, the second argument to the .map predicate function is the index of the array element, so it will be 0, 1, 2, etc. I don’t really have an example where you would use this except possible for logging, so I’ll omit it here since it’s a possible antipattern.

reduce

reduce is similar to map in that it iterates over the source array and returns a new value. The difference is that the predicate to reduce takes an additional argument first. This argument is called an accumulator. The following arguments are the same as map: the current array element, the index, and the whole array. The predicate is often called a reducer function. Redux users may be familiar with this concept.

reduce is useful if you want to create a value from your collection. Return the accumulated value from your predicate, and that value will be the accumulator parameter for the next predicate call.

The simplest example for reduce is getting a sum. One important way that reduce differs from map is that the first call to the predicate takes the first and second array elements. In the example below, the predicate is called three times rather than four:

  1. acc will be 1, num will be 2
  2. acc will be 3 (1 + 2), num will be 3
  3. acc will be 6 (3 + 3), num will be 4
  4. The final value of 10 (6 + 4) is returned

In the example below, we pass a starting value to reduce. When you do this, the predicate will be called for each array element and the accumulator starts at the provided value:

  1. acc will be 0 (initial value), num will be 1
  2. acc will be 1, num will be 2
  3. acc will be 3 (1 + 2), num will be 3
  4. acc will be 6 (3 + 3), num will be 4
  5. The final value of 10 (6 + 4) is returned

You will want to provide an initial value in cases where the type of the array elements is different than the type of accumulator. We can use our pluck example to show this:

If we did not provide an initial value, the code would at first attempt to do { name: 'Andrew', stars: 50 } + 20 which will result in [object Object]20.

The result returned from reduce can be anything. We often want it to be an Object:

There’s also no reason reduce can’t return an array … even an array whose length is longer than the original array.

reduceRight

reduceRight is the same as reduce except that it starts at the end of the array and iterates backwards. This would be equivalent to reversing the array and then using reduce.

The example below is the same as the previous example except that Dory’s pets will be in the output array first.

Querying

The next category of Array operators I’ll go over are what I’ve dubbed the querying operators. Querying operators are also Iteration operators, so they follow the same rules of taking a predicate with particular arguments. Querying operators return a subset of the original array or some value based on what you’re querying.

All of the querying operators could be done with reduce, but many of the operator names have better semantics and are more convenient.

sort <- *modifies array in place*

sort is a bit of an old school operator that reorders the array according to the provided predicate. sort modifies the original array. If you want to get a new array that is sorted without modifying the original array, you can use the concat operator, e.g. const sortedArray = [...originalArray].sort(). This works because even though sort updates the array in-place it also returns the sorted array.

sort's predicate function is different than other operators since it only takes two arguments: the first element for comparison and the second element for comparison. The algorithm that sort uses is implementation dependent, so its time and space complexity cannot be guaranteed. Keep this in mind if you plan to use sort on large arrays.

In this way, sort does not necessarily iterate over each element in order since it will depend on the sorting algorithm. However, it still has to iterate over each element at least once, and sorting is typically a querying operator, so I’ve included it in this category.

The predicate to sort is optional. If you do not provide one, it will compare the elements as strings and order them based on their UTF-16 code unit values. Keep this in mind: the default comparison is not numeric. We can see how this can be tricky:

You would probably expect the numbers to be sorted as 1, 2, 4, 10, etc… instead they are treated as strings and sorted as 1, 10, 2, 20, etc…

The predicate to sort should return a number.

  • If it returns a negative number, the first element will be placed at an earlier index than the second element.
  • If it returns a positive number, the second element will be placed at an earlier index than the first element.
  • If it returns 0, the first and second elements retain their current positions, but they will still be sorted with respect to other values.

Also note that sort skips undefined values, and they are placed at the end of the array and the predicate is not called for them.

Since sorting is based off of positive and negative values, this makes sorting numbers very easy since you can just subtract them:

When sorting other values, you can compare the first and second element. Typically you will return -1 if the first element should come before the second and 1 if the second element should come before the first, but these could be any negative or positive numbers. You can also return 0 if the values match, but this often won’t matter for the purposes of sorting, so it’s frequently omitted.

You may use the typical < and > to compare dates and strings:

I always forget exactly whether -1 or 1 will make the first element come before or after the second, but here’s a fairly simple mnemonic: -1 comes before 1 on the number line. In the same vein, if the predicate returns -1, the first element will come first. Otherwise, the second element will come first.

filter

You can use filter to get an array of elements according to a filtering predicate. The array that you get is at most as long as the original array if all elements pass the filter, but it will often be shorter than the original array. It may return an empty array. filter does not modify the original array, and it retains the relative order of elements in the original array.

Conceptually, this filters out elements from an array that you don’t want.

The predicate function you pass to filter should return a boolean. If it’s true, the element is retained in the output array. If it’s false, it’s not retained and will not be in the output array.

Using filter(Boolean) is a common mechanism for filtering out falsey elements. This would be identical to filter(value => !!value).

Of course, you can provide any function you want.

startDate < new Date('2016-01-01') is a boolean.

If no array element passes the predicate, you’ll get an empty array.

The predicate takes an index and the original array as arguments as well.

some

some returns a boolean.

some is functionally equivalent to array.filter(predicate).length > 0. You can use some to determine that at least one element in the array satisfies some condition.

One advantage to some is that in case it encounters an element whose predicate is true, it will short circuit and not have to run the predicate on any of the remaining array values.

includes

includes returns a boolean.

includes is similar to some except that it does not take a predicate. It takes a value. It’s functionally identical to array.some(val => val === providedValue). Keep in mind that when comparing objects, {} != {} since objects are compared by their reference. If you’re looking to see if an array has an element that is an object that matches some values, use some.

every

every returns a boolean

every is similar to some, except that it will return true only if every array element satisfies the predicate.

It is functionally equivalent to array.filter(predicate).length === array.length.

Similar to some, every will short circuit if it encounters an element that returns false from the predicate.

If you don’t think about it too much, you might think that array.some(predicate) === !array.every(predicate), but this is not true. For example, [false].some(Boolean) and [false].every(Boolean) are both false, and [true].some(Boolean) and [true].every(Boolean) are both true. They function in a similar fashion, but they’re checking for different things.

find and findIndex

find returns a single element from the array. I’ve included findIndex in the title since they work exactly the same except that find returns the array element itself and findIndex returns the index where the element sits. I’ll refer only to find from now on, but everything I say will apply to findIndex as well.

find takes a predicate like other iteration operators. If the predicate returns a truthy value, find will short circuit and return the current array element. Otherwise, it will move on to the next array element. If no elements match (all calls to the predicate return a falsey value), find will return undefined.

Note that find returns the first matching element even if there is more than one matching element. If you want to get all matching elements, use filter. If you want to do some equality check on the found value, use some.

indexOf

indexOf does not take a predicate. It is something like a fusion of findIndex and includes. It will return the index of the first element that matches the provided value. Keep in mind that when comparing objects, {} != {}. If you need to get a value from an array of objects, you probably want findIndex.

There is also a lastIndexOf that will return the last element that matches the provided value. There is no analog for find and findIndex. If you want the last matched element for either of those, use reverse first.

Transformation

Transformations are operators that modify the array usually according to some common functionality. Not all transformations actually modify the original array (I’ll indicate the ones that do below). Many of these can be done by reduce instead, but the methods below are more semantic and convenient.

reverse <- *modifies the source array*

This simply reverses the array. It takes the last array element and moves it to the first position, takes the second to last array element and moves it to the second position, and so on. The first element of the original array will be moved to the last position.

You can use the spread operator to get a separate, reversed array, as in reversedArray = [...array].reverse(). reverse returns the reversed array even though it also modifies the original array.

join

Join iterates over each array element, returns its string representation, and puts some separator between each array value. By default, the separator is a comma (with no spaces).

This is sort of the opposite of the string split operator which creates an array by dividing up the string along the provided separator where each array element is a substring between the separators.

This is useful for an array of strings. If you have an array of objects, you could use map first to map to an array of strings and join that.

push, pop, shift, and unshift <- *modify original array*

These operators are specifically for adding and removing elements from an array. These all modify the array itself.

push adds an element to the end of the array (the added value becomes the last element). Nowadays, push is probably less common than spread. Using array.push(value) is a non-immutable version of [...array, value]. push returns the length of the new array. You can push more than one value at a time.

pop removes the element at the end of the array (the last element of the array). pop modifies the original array and returns the removed value.

shift is similar to pop except that it removes the first element from the array. It modifies the array in place and returns the removed value.

unshift is similar to push except that adds an element to the start of the array (the added value becomes the first element). Similar to push, you can do an unshift operation immutably using spread: [value, ...array]. You can unshift more than one value at a time. Don’t be confused by the un in the name of this operator: it adds a value to the array. Like push, unshift returns the length of the updated array.

slice

slice takes out a slice or chunk of the array. slice takes up to two arguments: the starting index to slice from and the ending index to slice up to.

Both arguments are optional. If you provide no arguments, slice will return a shallow copy of the array as if you used [...array]. The element at the starting index will be returned as part of the slice. If you do not include an ending index, all elements from the starting index to the end of the array will be included. If you provide an ending index, it will not be included in the array.

You can use slice to help you immutably add an element into an array by combining it with spread.

splice <- *modifies original array*

I would forego using splice at all and use slice with spread instead. splice is similar to slice in that it returns a slice or chunk of the original array. However, you can also optionally insert new elements where the slice was removed.

I’m mentioning splice because I think it was more commonly used before immutability really took off as a concept, so I won’t provide an example here. If you want to remove elements from an array and add elements in place, use spread and slice instead. We can modify our example above to insert Bryan where Andrew is by changing the slices we use.

In my opinion, this also makes it easier to see how the array is being modified.

About Immutability

Immutability has become a core concept in JavaScript development. Generally, rather than modify an original array, you can create a new array that is a copy of the source array. This allows you to get new representations of the original array without actually modifying it. Immutability is useful in component-driven frameworks such as React and Angular with concepts of change detection. Changing an existing object will not trigger change detection for comparison. Instead, you can create a new array with modified elements.

Some examples include:

  • array.push(value) -> [...array, value]
  • array.pop() -> array.slice(array.length — 1)
  • array.shift() -> array.slice(1)
  • array.unshift(value) -> [value, ...array]

Note that the spread operator, map, et. al. create shallow copies of the original array. That is to say that although you may not modify the original array, you may modify array elements if they are objects.

You should write code immutably at every level, creating copies where they are needed.

In some cases, it may be convenient to create a deep copy of objects (including arrays). You can use tools such as immer or lodash to help with this, but using the spread operator is a simple, native way to create shallow copies of arrays and objects.

Conclusion

There are many more array operators than what I listed here, and I’d recommend that you review MDN’s documentation on Arrays to see if there is an operator you need or can use for your given situation.

I hope that this article has made some people more familiar with some of the excellent options for common use cases for arrays / collections that are available to you. There are other excellent libraries like lodash and RxJS that provide these operators and many more useful operators for working with other kinds of collections including arrays.

--

--