Definitive Guide to JavaScript Arrays (Part I): methods

Ammar Halees
14 min readMay 12, 2019

--

The objective of this article (Part I and II) is to summarize most of what you practically need to know about JS arrays

Disclaimer

This article will be among a series of article on JavaScript and front-end development that I’m making. These articles are meant to be relatively simple, concise and free of excess jargon. Also, please note that not all array methods are mentioned (You can find them here JavaScript Array Reference), only the ones I believe are practically more commonly used than others.

Also, if you find any mistakes, please give me a heads up by pointing them out in the comments or by sending me an email: haleesammar@gmail.com

Background

An array is a data structure of an indexed collection of values. Unlike many other languages (such as C++ and C); JS array elements are not confined to being of the same data type. Different elements within the same array may have different types.

Let’s begin..

Declaring Arrays

There are 3 different ways for creating arrays in JS:

1. Array Literal syntax (Recommended)

const myArray = ["Tommy",()=>console.log("function element"),2,true]

Notice how the array elements are not necessarily of the same data type. One of them is even a function, which by the way, may be invoked by first accessing the index of the function element within the array, and then invoking it as a function:

myArray[1]()  //function element

2. Using the “new” keyword (Not conventional)

var items = new Array("milk","water");

3. On the fly (Created when a function is called)

An array is created when it’s passed to a function that takes an array as its parameter. Or more precisely; the array is created and gets stored in memory when that function is evoked and currently resides on the execution stack.

function displayArray(passedArray){console.log(passedArray);}displayArray([1,2,3]);

In the above code; an array got created as soon as a call is made to displayArray .

Before moving any further: Remember that:

Array elements can be accessed (for either read or write) using square brackets.

myArray[0] = "star"  //Writeconsole.log(myArray[0])  //Read

Array Methods

There are several ways of categorizing array methods. I found the most convenient way is to categorize them based whether or not these methods mutate (change) the original array. I then sub-categorized these methods based on their return type.

1. Array Methods which do not alter the original array

The first row is, of course, the return type of the method

2. Array Methods which alter the original array

Most array methods require at least 1 mandatory argument. Most also have -optional arguments which can be useful. Other methods require non at all.

In many cases; that mandatory argument is a callback function, which will run for each element in the array, with that element as its input.

It is conventional to use ES6’s arrow function syntax for the cb function.

As a developer, you will find yourself using some methods more frequently than others. Let’s examine some of these methods in more detail:

map

  • Purpose: Iterate over the array elements.
  • Input: A callback function to run for each array element.
  • Output: A new array of mapped elements.
  • Modifies the Original Array? No.
const arr = [1, 2, 3, 4, 5, 6];console.log(arr.map(n=> n*2));   // [2, 4, 6, 8, 10, 12]

filter

  • Purpose: Iterate over the array elements, filtering out certain elements
  • Input: A callback function to run for each array element. This function would have some kind of conditional logic to determine whether or not an element fits a specific filtering criteria.
  • Output: A new array of filtered elements. Elements that fulfill the condition.
  • Modifies the Original Array? No.
const arr = [1, 2, 3, 4, 5, 6];console.log(arr.filter(n=> n > 3));   //[4, 5, 6]

concat

  • Purpose: Merge two or more arrays.
  • Input: Arrays to be merged with the original array.
  • Output: A new merged array.
  • Modifies the Original Array? No.
const arr1 = [1, 2, 3];const arr2 = [4, 5, 6];console.log(arr1.concat(arr2))   //[1, 2, 3, 4, 5, 6]

Watch out because order matters:

const arr1 = [1, 2, 3];const arr2 = [4, 5, 6];const arr3 = [7, 8, 9];console.log(arr2.concat(arr1,arr3))    // [4, 5, 6, 1, 2, 3, 7, 8, 9]

splice

  • Purpose: Removes/Adds elements at a specified index in the array.
  • Input:
  1. Start Index (Mandatory)
  2. Number of elements to be removed starting from the specified index (Optional)
  3. New elements to be add in place of the removed elements (Optional)
  • Output: An array of the removed elements.
  • Modifies the Original Array? Yes.
const array1 = ["a", "b", "c", "d"];console.log(array1.splice(1,1));   //["b"]console.log(array1);   //["a", "c", "d"]

In the above example: we passed 1 as the start index and 1 as the number of elements to be removed starting from there, but we didn’t specify any elements to be inserted in place of the removed elements. This code above will result in the following:

a) The removal of the element at index 1.

b) The indexes of all array elements after the removed element will be updated.

c) The length of the original array is updated.

Therefore, array1 becomes: [“a”, “g”, “c”, “d”].

What if we don’t want to remove elements, only add?

We simply set the second argument to 0 (Which basically means that we want to remove 0 elements), and set the rest of the arguments with our desired elements to-be-added:

const array1 = ["a", "c", "d"];array1.splice(1,0,"b");console.log(array1);    //["a", "b", "c", "d"]

What if we specify only the mandatory first argument?

In that case: all elements starting from the start index, will be removed:

const array1 = ["a", "b", "c", "d"];array1.splice(2)console.log(array1);  // ["a", "b"]

slice

  • Purpose: Removes elements at a specified index in an array.
  • Input:

1.Start Index (Mandatory) ,

2. Number of elements to be removed starting from the specified index (Optional)

  • Output: An array of the removed elements.
  • Modifies the Original Array? No.
const array1 = ["a", "b", "c", "d"];console.log(array1.slice(1,2));   //["b"]console.log(array1);   //["a", "b", "c", "d"] 

slice is quite similar to splice, except that:

  1. slice only removes elements, does not add.
  2. slice does not modify the original array on which the method is applied, instead, it creates a copy of the “sliced” array elements.

One odd thing about slice which you may have noticed is that the second optional parameter is count -1. For example, consider the code above:

You’d expect that 2 elements to be removed starting from index 1. But in reality; the second parameter is count -1. This is just something to keep in mind when using this method.

forEach

  • Purpose: Iterate over the array elements.
  • Input: A callback function to run for each array element.
  • Output: undefined
  • Modifies the Original Array? No.
const myArr = [1, 2, 3, 4, 5, 6];myArr.forEach(n=> console.log(n*3));   // 3 6 9 12 15 18console.log(myArr.forEach(n=> n*3 ))    // undefined

forEach is similar to other iterative methods like map and filter But unlike those: it does not return an array: it returns undefined .So use it when you want to produce a side-effect and not when you need a return value.

find

  • Purpose: Finds, then returns the first element in the array that passes the condition provided by the callback function.
  • Input: A callback function that will run for each array element and test that element against a condition (Mandatory)
  • Output: The first element that passes that condition (if found). Or undefined if no such element exists.
  • Modifies the Original Array? No.
const arr = [1, 2, 7, 4, 3, 5];console.log(arr.find( x=> x > 3 ));   // 7

When the element is found for the first time; the method stops executing and returns.

findLast

  • Purpose: Finds the last element in an array that passes the condition provided by the callback function.
  • Input: A callback function that will run for each array element and test that element against a condition (Mandatory)
  • Output: The first element that passes that condition (if found). Or undefined if no such element exists.
  • Modifies the Original Array? No.
const arr = [1, 2, 7, 4, 3, 5];
console.log(arr.findLast( x=> x > 3 ));   // 5

When the element is found for the first time; the method stops executing and returns.

indexOf

  • Purpose: Finds, then returns the index of the first element in the array that matches the passed element.
  • Input:
  1. The element whose index we want (Mandatory)
  2. Start Index (Optional).
  • Output: The index of the element (if found) or -1 if none is found.
  • Modifies the Original Array? No.
const arr = [1, 3, 7, 4, 3, 7];console.log(arr.indexOf(7));   // 2console.log(arr.indexOf(9));  // -1

Note the following:

  • The passed element may only be a primitive value (String, Number etc.) . For instance we can’t attempt to get the index of an object element within the array using the indexOf method. There’s another method for that.
  • Notice that in the above code: there are multiple occurrences of 7 , but the indexOf method will return the index of the first found 7 only.

findIndex

  • Purpose: Finds, then returns the index of the first element in the array that passes a condition provided by the callback function.
  • Input: A callback function that will run for each array element and test that element against a condition (Mandatory)
  • Output: The index of the element (if found) or -1 if none is found.
  • Modifies the Original Array? No.
const arr = [1, 3, 7, 4, 3, 5];console.log(arr.findIndex(x=> x > 3));  // 2

Similar to indexOf ; This method’s job is to find the index of a certain element.

But what makes findIndex different is that:

  1. It takes a callback function as a parameter enabling a more dynamic approach to search. This is especially useful when you don’t know exactly what you’re looking for and you only know that it fits within a known approximate range of description.
  2. With findIndex ; we are able to search for more complex types such as objects through maybe a property attached to them.

Examine the following:

Searching through an array of Objects, using the age property as our anchor.

const arr = [{name:"Tim",age:22},{name:"Sally",age:36},{name:"Carol",age:52}];console.log(arr.findIndex(element=>element.age>27)); // 1 

includes

  • Purpose: Checks whether or not an array contains the passed element.
  • Input:
  1. The element whose existence in the array we’re investigating (Mandatory)
  2. The index from which to start (Optional).
  • Output: true if the element is found. false if the element is not found.
  • Modifies the Original Array? No.
const arr = [1, 3, 7, 4, 3, 0];console.log(arr.includes(3,5));  //false

The above example will return false because although the array does include a 3 , twice, we defined the start index to be 5 . No 3 can be found in the array starting from index 5 and on.

This method is quite useful. However, there’s a pitfall in the fact that: just like in the indexOf case; we are not able to pass a callback function. We have to pass the specific element; a primitive. This prevents us from performing more complex querying i.e. querying that is reliant on a condition or a range. This also prevents us from searching through an array of complex types such as objects.

We’ll see how we can take care of that in the upcoming method…

some

  • Purpose: Checks whether or not an array contains at least one element which passes the condition provided by the callback function
  • Input: A callback function that will run for each array element and test that element against a condition (Mandatory)
  • Output: true if at least one element passes the condition provided by the callback function. false otherwise.
  • Modifies the Original Array? No.
const arr = [1, 3, 7, 4, 3, 5];console.log(arr.some(x=>x<3));   //true

The above example will return true because at least one element in the array is less than 3 .

some can be used in the same way as includes but with extended functionality by allowing us to pass call back function and therefore enabling us to search through an array of objects.

every

  • Purpose: Checks if all the elements in the array pass the condition provided by the callback function
  • Input: A callback function that will run for each array element and test that element against a condition (Mandatory)
  • Output: true if all the elements pass the condition provided by the callback function. false otherwise.
  • Modifies the Original Array? No.
const arr = [1, 3, 7, 4, 3, 5];console.log(arr.every( x=> x < 2));    //false

push

  • Purpose: Adds items to the end of the array
  • Input: Items to be added: item1, item2, …, itemX
  • Output: New length of the array
  • Modifies the Original Array? Yes.
const arr = [1, 3, 7, 4, 3, 5];arr.push(6,7,8);console.log(arr);   // [1, 3, 7, 4, 3, 5, 6, 7, 8]

unshift

  • Purpose: Adds items to the start of the array
  • Input: Items to be added: item1, item2, …, itemX
  • Output: New length of the array
  • Modifies the Original Array? Yes.
const arr = [4, 5, 6];arr.unshift(1,2,3);console.log(arr);  //  [1, 2, 3, 4, 5, 6]

pop

  • Purpose: Removes items from the end of the array
  • Input: none
  • Output: The removed item
  • Modifies the Original Array? Yes.
const arr = [1, 3, 7, 4, 3, 5];console.log(arr.pop());  //5console.log(arr);   // [1, 3, 7, 4, 3]

shift

  • Purpose: Removes items from the start of the array
  • Input: none
  • Output: The removed item
  • Modifies the Original Array? Yes.
const arr = [1, 3, 7, 4, 3, 5];console.log(arr.shift())  // 1console.log(arr) // [3, 7, 4, 3, 5]

sort

  • Purpose: Re-arrange array elements in a particular order, based upon the return value of the compare function.
  • Input: Compare Function (Optional)
  • Output: A new sorted array
  • Modifies the Original Array? No.

Using sort without the compare function parameter

When we supply no parameters to sort ; it will revert to its underlying sorting mechanism: that is to convert the array elements intro strings and then to arrange them in an ascending order, based upon their corresponding UTF-16 code points (The character encoding scheme JavaScript uses).

But that puts a lot of limitations, doesn’t it? I mean, what if we don’t want to sort our string array in ascending order? What if our array elements aren’t even strings?

And besides, in UTF-16 encoding code point representation: Capital letters are chronologically lower than small letters. Of course you could work around that by converting the string to lowercase using toLower​Case .

Using sort with the compare function parameter

Now that we have established that in order to perform any meaningful or real world sorting operation on arrays you need use a compare function; let’s see how they work:

The compare callback function takes two parameters representing the two numbers being compared (Because that’s part of how the in-space algorithm works; Only two elements are compared at a time) and returns either a positive number or a negative number or 0. Based on the return value of the compare function: sort will decide how to arrange the elements:

  • If the return value of compareFunction(x,y) is negative: place x in a lower index than y.
  • If the return value of compareFunction(x,y) is positive: place x in a higher index than y.
  • If the return value of compareFunction(x,y) is 0: don’t change order.

So, you as a programmer , are tasked with designing how sort should behave, by deciding when the compare function returns (- or+ or 0).

Example : Sorting an array of numbers in a descending Order:

const compare= (a,b) =>{  if (a>b) return -1;   return 11 }

A nice shortcut to the above code is to simply return the subtract result between a and b because that will either be (- or+ or 0) and so it will give the same result.

const compare= (a,b) => b - a

Just a thought: What if there are undefined values in your array? What happens to them?

undefined values get sorted to the end of the array.

Reduce

  • Purpose: Reduce an array into a single value by accumulating its elements.
  • Input:
  1. Reducer function (Mandatory)
  2. initial value (Optional)

The reducer function takes the following input:

  1. Accumulator (acc) — mandatory: A value which persists across each iteration. It also gets updated at each iteration. At the end: the accumulator value becomes the result of the reduce.
  2. Current Value (cur)- mandatory : the current element we are at.
  3. Current Index (idx)
  4. Source Array (src)
  • Output: A single value
  • Modifies the Original Array? No.
const arr = [1, 2, 3, 4];const reducer= (accumulator //accumulates the value return by callback at each invocation, currentVal) =>{return accumulator + currentVal;}

If no initial value is provided:

Then the accumulator is initialized as the fist element and the currentval starts at the second element.

Bonus: Modern Array Methods and Techniques

Recent JS releases have brought along an interesting number of new array features and methods:

flat

  • Purpose: Gets rid of nesting in arrays
  • Input: The depth of nesting to be flattened (default is 1 level deep)
  • Output: The new flattened array.
  • Alters Original Array? No, It creates a shallow copy.

As the name suggests; flat flattens the array.

Prior to that, we used concat.apply :

const arr = [1, 7, ["a","b","c"], 3, 5];console.log([].concat.apply([], arr))  // [1,7,"a","b","c",3,5]

flat provides a cleaner, more readable syntax. Let’s take a look:

const arr = [1, 7, ["a","b","c"], 3, 5];console.log(arr.flat())  // [1, 7, "a", "b", "c", 3, 5]

Flattening is useful in scenarios where it is needed to expose the elements of an inner array, to the outer array. But of course, it depends on the case as maybe it is within your intention to create a nested array.

If there’s more nesting which you would like to be flattened; you can always specify the depth by passing it as a parameter:

const arr = [1, 7, ["a",[9,8,7],"c"], 3, 5];console.log(arr.flat(2)) // [1, 7, "a", 9, 8, 7, "c", 3, 5]

In the first example the depth was 1, but that’s the default so we don’t need to pass anything.

Additional Usage for flat :

flat can also be used to get rid of gaps (empty elements) in the array:

const arr = [1 , , 2 , 7 , , 3];console.log(arr.flat())   // [1, 2, 7, 3]

flatMap

  • Purpose: Gets rid of nesting in arrays upon mapping an array.
  • Input: The depth of nesting to be flattened (default is 1 level deep)
  • Output: The new flattened array.
  • Alters Original Array? No, It creates a shallow copy.

As the name suggests; flatMap combines the functionality of flat and map .flat is performed immediately after map. This has many benefits especially in linear algebra where there’s frequent mapping to 2-D matrices.

const arr = [1, 2, 3];console.log(arr.flatMap(x => [ x , x*2 , x*3 , x*4 ]) )  //[1, 2, 3, 4, 2, 4, 6, 8, 3, 6, 9, 12]

copyWithin

  • Purpose: Shallow copies a part of the array to another part within the same array, without altering the length of the array.
  • Input:
  1. Target index : Where should the copied portion get transferred to.
  2. Start Index (Optional): What is the start index of the copied portion.
  3. End index (Optional): What is the end index of the copied portion.
  • Output: The new array.
  • Alters Original Array? Yes.

The method is useful when you need to perform relatively complex array alterations in which parts of the array must replace others. Check out this example:

var arr = ['a', 'b', 'c', 'd', 'e'];console.log(arr.copyWithin(1, 2, 4));   //["a", "c", "d", "d", "e"]

The start index is 2 . The end index is 4 . The end index is count -1, so the copied portion is an array from index 2 to index 3 (2 elements).

The target index is 1; so that copied portion will be placed in the array starting from index 1 . This will cause two elements to be replaced with the copied array elements, starting from index 1. The result is:

["a", "c", "d", "d", "e"]

Notice how, elements before the start index and elements after the end index -1 , were not affected.

Different (exceptional) argument cases:

  • If no start index is specified: The portion will be copied from index 0 and will end at the specified end index (if any):
var arr = ['a', 'b', 'c', 'd', 'e'];console.log(arr);   //["a", "b", "c", "d", "e"]console.log(arr.copyWithin(1,null,2));  // ["a", "a", "b", "d", "e"]
  • If no end index is specified: The portion will be copied from the specified start index (if any)and will end at the indexArray.length (because remember : end index is count -1)
var arr = ['a', 'b', 'c', 'd', 'e'];console.log(arr.copyWithin(1,2));   //["a", "c", "d", "e", "e"]
  • If neither a start index or an end index is specified (Effectively no arguments are passed): Then the copied portion would be the whole array, and that would be useless.
var arr = ['a', 'b', 'c', 'd', 'e'];console.log(arr.copyWithin());  //["a", "b", "c", "d", "e"]
  • If a negative target value is supplied; then the index will be counted backwards (starting from the end), with -1 (not 0 ,cause that’s reserved to the forward index count) being the first reverse index.
var arr = ['a', 'b', 'c', 'd', 'e'];console.log(arr.copyWithin(-2,1,3));   //["a", "b", "c", "b", "c"]

End of Part I.

Stay tuned for part II where we’ll discuss more array methods as well as different topics surrounding arrays. Thank you for sticking around \( * ‘- ‘ *)/

Find me at:

--

--