Array Data Structure in Javascript — Search, Sort, Filter, Map, & Reduce

Featured image for “Array Data Structure in Javascript — Search, Sort, Filter, Map, & Reduce”

The array is a powerful data structure and has a large variety of applications. Let’s continue our exploration by diving into more array methods and possible algorithms to further enrich your knowledge.

Video Version of This Article

This post is a more detailed article version of the Array Data Structure Series on Youtube that you can check if you prefer videos.

Watch Videos

Check the Previous Array article

This article is Part II of the initial array data structure introductory article you must check to understand how it works and much more.

Part 1 — Intro to Array Data Structure

Searching and Finding Array Items

You can access any item if you know its address — aka index — but sometimes you don’t know at which index the item is so you need to find it. Other times you just want to know at which index the item is and for both of the situations, the JavaScript array comes with the “find” and the “findIndex” for the job.

The “find” will return “undefined” if no match otherwise the item that matched the condition. The “findIndex” will return “-1” if no match otherwise the index of the item that matched the condition. Both accept a callback that must return “true” or “false” whether the item is what you want or not. A “truthy” or “falsy” value is also valid.

const todoItems = [
  {id: 0, title: 'pick kids at school'},
  {id: 1, title: 'go to the gym'},
  {id: 45, title: 'study for the exam'}
];

todoItems.find(todo => todo.id === 45);
// returns
// { id: 45, title: 'study for the exam' }

todoItems.findIndex(todo => /gym/g.test(todo.title));
// returns 1

The “find” and “findIndex” methods do a linear search, meaning, they always go from start to finish quitting as soon as a first match is made. They only give you a single matched item but what if you want all matches?

We can use the “filter” method for that and ill show you how later. For now, below is an example of a custom “findAll” function using everything we learned so far about arrays.

const numbers = [45, 12, 99, 2, 10, 78, 34];

function findAll(list, cb) {
  const matches = [];
  let i = 0;

  for(const item of list) {
    if(cb(item, i, list)) {
      matches.push(item)
    }
    i++;
  }

  return matches;
}

findAll(numbers, n => n > 50);
// returns [ 99, 78 ]

Checking if Array contains certain Items

There will be situations in which you simply want to confirm that a certain item is in the array and don’t care where. You can use the find methods you learned above to check if they return something different than “undefined” or “-1” to confirm but there is a better way for that. The Array comes with the “includes”, “indefOf”, and “lastIndexOf” methods for the job.

const numbers = [45, 12, 99, 2, 10, 78, 34];

numbers.includes(99); // true
numbers.includes(300); // false
numbers.indexOf(99); // 2
numbers.indexOf(300); // -1

Both, “includes” and “indexOf”, work great with primitive values but operate on instances for anything else so if you have an array of non-primitives, the results will be different, as you can see below.

const gymTodo = {id: 1, title: 'go to the gym'};

const todoItems = [
  {id: 0, title: 'pick kids at school'},
  {id: 45, title: 'study for the exam'},
  gymTodo
];

todoItems.includes({id: 1, title: 'go to the gym'}); // false
todoItems.includes(gymTodo); // true
todoItems.indexOf({id: 1, title: 'go to the gym'}); // -1
todoItems.indexOf(gymTodo); // 2

Creating an object on the fly is simply a new instance even though it may contain the same keys and values of something you are looking for inside the array, therefore, it won’t match.

The “lastIndexOf” method works the same way as the “indexOf” but it starts looking from the end of the array instead. Both accept a second optional argument which is the index from which to start looking from.

const numbers = [45, 12, 99, 2, 10, 45, 78, 34];

numbers.indexOf(45); // 0
numbers.lastIndexOf(45); // 5
numbers.indexOf(45, 5);
// starts looking from index 5
// to the end of the array
// returns 5

numbers.lastIndexOf(45, 4);
// starts looking from index 4
// to the beginning of the array
// returns 0

With this second argument option, we can create a custom function that lets us collect all the matching indexes.

const numbers = [45, 12, 99, 56, 10, 45, 78, 34];
function allIndexOf(list, index, fromIndex = 0) {
  const matches = [];

  while(fromIndex < list.length) {
    fromIndex = list.indexOf(index, fromIndex);

    if(fromIndex < 0) break;

    matches.push(fromIndex);
    fromIndex += 1;
  }

  return matches;
}

allIndexOf(numbers, 45);
// returns [ 0, 5 ]

You may also need to know whether an item exists at least once in the array or if all the items in the array match certain criteria. For that kind of verification, you can use the “some” and “every” method.

const numbers = [45, 12, 99, 56, 10, 45, 78, 34];
numbers.some(n => n === 10); // true
numbers.every(n => n > 10); // true

Both, “some” and “every” accept a callback that must return a “truthy” or “falsy” value. The “some” method quits as soon as it makes the first match and the “every” method will check all items in the array.

Note: You can use “some” instead of “every” since if some item is different it cannot return true for “every” and it will save you from going through the entire list sometimes.

Sorting Array Items

The array prototype comes with the “sort” method but can give you weird results. Luckily, it accepts a callback function which you can use to gain control over the sorting. This method is not pure which means it changes your array in place even though it returns the sorted array.

For example, the below code sorts the numbers array but does not return an expected result. This is because it transforms everything into a string if a callback function is not provided. So, 10 becomes “10” and 2 becomes “2” and when comparing them character by character, “1”(first character in “10” string) for sure comes before “2” therefore it makes 10 appear before 2 in the returned array.

const numbers = [45, 12, 99, 2, 10, 45, 78, 34];
numbers.sort();
/* numbers become
[
  10, 12,  2, 34,
  45, 45, 78, 99
]
*/

To fix that we can provide a callback and use a simple trick to sort numbers ascending or descending.

const numbers = [45, 12, 99, 2, 10, 45, 78, 34];
numbers.sort((a, b) => a - b);
/* sorts ascending
[
   2, 10, 12, 34,
  45, 45, 78, 99
]
*/
numbers.sort((a, b) => b - a);
/* sorts descending
[
  99, 78, 45, 45,
  34, 12, 10,  2
]
*/

The callback must return a negative number(-1 by convention) to indicate that the left side is less than the right side, a positive number(1 by convention) to indicate the left side is greater than the right side, a zero to indicate that both sides are the same.

That is why the above code example works because when we do “a - b” it returns either a negative, positive, or zero. We can also be more explicit and returns those numbers instead, as you can see below.

const people = [
  {name: 'A', age: 12},
  {name: 'K', age: 34},
  {name: 'b', age: 12},
  {name: 'J', age: 24},
  {name: 'A', age: 34},
  {name: 'Z', age: 18},
  {name: 'E', age: 56}
];

people.sort((personA, personB) => {
  if(personA.age < personB.age) return -1
  if(personA.age > personB.age) return 1

  return 0;
});
/* people array becomes
[
  { name: 'A', age: 12 },
  { name: 'b', age: 12 },
  { name: 'Z', age: 18 },
  { name: 'J', age: 24 },
  { name: 'K', age: 34 },
  { name: 'A', age: 34 },
  { name: 'E', age: 56 }
]
*/

Reverse & Shuffle Array Items

The same way you want to sort the items in an array into a specific order you may want to also reverse the items as well as create a random order of items. The Array prototype exposes a “reverse” method but no method that allows you to shuffle.

The “reverse” method is not pure. It will reverse your array in place even though it returns the reversed array.

const numbers = [45, 12, 99, 2, 10, 45, 78, 34];

numbers.reverse();
/* numbers become
[
  34, 78, 45, 10,
   2, 99, 12, 45
]
*/

To reverse an array purely — without changing the original array, we can create our own reverse function which goes over the list and “unshift” the internal array with the list item. The “unshift” method adds the item to the beginning of the array as we iterate the list.

const numbers = [45, 12, 99, 2, 10, 45, 78, 34];
function reverseList(list) {
  const reversedList = [];

  for(const item of list) {
    reversedList.unshift(item);
  }

  return reversedList;
}

reverseList(numbers);
/* returns
[
  34, 78, 45, 10,
   2, 99, 12, 45
]
*/

console.log(numbers)
/* numbers dont change
[
  45, 12, 99,  2,
  10, 45, 78, 34
]
*/

Shuffle an array there are many ways you can approach it. A famous shuffle algorithm is the Fisher-Yates shuffle algorithm which is used in the example below to return a shallow shuffled copy of the array without changing the original.

const numbers = [45, 12, 99, 2, 10, 45, 78, 34];

function shuffleList(list) {
  // make a shallow copy of the list
  const shuffled = [...list];

  for(const index in list) {
    // turn index into a number
    // since it is read as string
    const i = Number(index);
    const j = Math.floor(Math.random() * (i + 1));
    [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
  }

  return shuffledList;
}

const shuffledNumbers = shuffleList(numbers);
// returns a shuffled array

console.log(numbers)
/* numbers dont change
[
  45, 12, 99,  2,
  10, 45, 78, 34
]
*/

The above example takes advantage of the array destructuring assignment syntax which by assigning an array to another with items in different position cause the items to swap places.

const ab = [3, 4];

[ab[0], ab[1]] = [ab[1], ab[0]];
/* ab becomes [4, 3];

Turn Array into a string

Turning an array into a string can prove to be useful in many situations. There are 3 methods we can use to do that and they all serve different purposes.

  • toString: We can use the toString” method to simply turn an array into a string. It will go through each item and call a “toString” method to get the string version of the items. This does mean that you may get errors or unexpected results depending on the items in the array.
const mixArray = [12, "sample", function() {}, {test: true}];

mixArray.toString();
// results in
// '12,sample,function () {},[object Object]'

const arrayWithSymbolItem = [Symbol('secret')];

arrayWithSymbolItem.toString();
// throws error
// TypeError: Cannot convert a Symbol value to a string
  • toLocaleString: the “toLocaleString” is an awesome method that allows for some amazing things and you should read about and more reach examples by learning how to work with dates and localization in JavaScript, for example. A good example is if you have an array of prices and want to add the currency symbol to them. The “toLocaleString” takes the language and formatting options to use which you can learn more about by checking the Intl DateTimeFormat method.
const prices = [7, 500, 8123, 12];

prices.toLocaleString(
   'en-US',
   { style: 'currency', currency: 'USD' }
);
// returns
// "$7, $500.00, $8, 123.00, $12.00"
  • join: The “join” is a powerful method in the sense it allows you to join the array items together and pick the delimiter to use in between the stringified items.
const peopleNames = ['John', 'Peter', 'Jane'];

peopleNames.join(', ');
// returns: 'John, Peter, Jane'

Map & Filtering Items

Mapping is an awesome way to transform and create a new array at the same time without changing the array length. Filtering allows you to grab only the items that match your criteria. Both, “map” and “filter” methods, are pure array methods, meaning, they do not change the original array.

const numbers = [45, 12, 99, 2, 10, 45, 78, 34];
const doubledNumbers = numbers.map(n => n * 2);
/* double the numbers
[
  90, 24, 198,  4,
  20, 90, 156, 68
]
*/
const oddNumbers = numbers.filter(n => n % 2 !== 0);
/* grab odd numbers
[ 45, 99, 45 ]
*/
// numbers remain intact

In the previous article, I mentioned that the “from” method can be superior to the “map” method at times and the reason is that you don’t need to start from an array to map something when using the “from” method. If all you want is a new array with specific values, the “from” method only cares if the object is iterable or an iterator where “map” is only available through the array instance prototype, meaning, you must have an array first.

The argument against that is that anything that is iterable can be changed into an array easily so, it comes down to preference. In general, the “from” is meant to create arrays from something else and map it if you want to. The “map” is great when you want to transform an existing array into another.

Note: Using the “from” and “map” together is redundant.

const set = new Set([34, 23, 10]);
const iterableObj = {
  items: [34, 23, 10],
  [Symbol.iterator]() {
    const items = this.items;
    return {
      current: 0,
      next() {
        if(this.current < items.length) {
          return {value: items[this.current++], done: false}
        }

        return {done: true}
      }
    }
  }
}

// using the "from"
console.log(
  Array.from(set, n => n * 2),
  Array.from(iterableObj, n => n * 2),
  Array.from('sample', n => n + '_'),
  Array.from({length: 10}, (_, i) => i+1),
)

// using the "map"
console.log(
  [...set].map(n => n * 2),
  [...iterableObj].map(n => n * 2),
  [...'sample'].map(n => n + '_'),
  Array(10).fill().map((_, i) => i+1),
)
// both will produce the same arrays

The “filter” method can be used instead of the “findAll” function that I showed you in the code example at the beginning of this article. It will return a new array containing only the items that matched the condition.

const numbers = [45, 12, 99, 2, 10, 78, 34];
numbers.filters(numbers, n => n > 50);
// returns [ 99, 78 ]

Flatting Array Items

In case you have nested arrays, the Array prototype comes with the flat” method to straighten that up. It returns a new array instead of changing the original array and takes an optional argument to indicate how deep you want it to flat the array.

const numbers = [
  [[34, 23], [23, 67]],
  [[12, 90], [33, 53]]
];
numbers.flat();
/* returns
[
  [ 34, 23 ], [ 23, 67 ],
  [ 12, 90 ], [ 33, 53 ]
]
*/
numbers.flat(2);
/* returns
[
  34, 23, 23, 67,
  12, 90, 33, 53
]
*/

There is also a flatMap” method which is a combo of “flat” and “map”. It can only flat the array to a depth of one but you can always call it again in the inner array. It can be great to transform an array at the inner group level like in the example below where I am calculating the sum of each number pairs.

const numbers = [[[34, 23], [23, 67]], [[12, 90], [33, 53]]];
numbers.flatMap(([group1, group2]) => [
  group1[0] + group1[1],
  group2[0] + group2[1]
]);
// returns [ 57, 90, 102, 86 ]

Reduce Array

The “reduce” method is probably the hardest one to read and one that is often questioned if it is really needed. Nonetheless, it is a super powerful method that allows you to turn your array into anything else you want. You may use to apply concepts like filtering and mapping which makes it the most powerful and flexible Array method.

const numbers = [45, 12, 99, 2, 10, 78, 34];
numbers.reduce((accumulator, number) => {
  return accumulator + number;
}, 0);
// returns 280

The example above is a cliche one often used to explain how it works. So what is going on is we call “reduce” with 2 arguments, the callback, and a value to initialize it with, in this case, zero. The callback is called for each item in the array and it contains the “accumulator” as the first argument and the item iterated. The “accumulator” is the 0(zero) we initialized it with and on each iteration we are updating it by returning it plus the item, in this case, number.

To better show you what is going on, here is what “reduce” is doing in plain simpler code.

const numbers = [45, 12, 99, 2, 10, 78, 34];
// same as second argument in "reduce" call
let accumulator = 0;
// reduce loops the array
for(const number of numbers) {
  // same as return
  accumulator += number;
}
console.log(accumulator); // 280

Any “reduce” can be simplified to a simple for loop but the convenience is that it returns you the tracked result, prevents you from creating loose variables, and can be less code at times. It can also be super powerful to collect a lot of data at once and format your array into something else more explicit.

The example below initializes an object with odd and even empty arrays. Inside the callback we check each item and add it to the appropriate list, updating the accumulator and returning it for the next iteration.

const numbers = [45, 12, 99, 2, 10, 78, 34];
numbers.reduce((acc, number) => {
  if(number % 2 === 0) {
    acc.even.push(number);
  } else {
    acc.odd.push(number);
  }

  return acc;
}, {odd: [], even: []});
/* returns
{ odd: [ 45, 99 ], even: [ 12, 2, 10, 78, 34 ] }
*/

Array methods “this” bind

During this article, you got exposed to a lot of array methods and most of them take a callback function, for example, “map”, “forEach”, “flat”, etc. What I did not mention is that some of them that take a callback also take a second argument which is the object to bind to the callback you passed.

If you ever feel the need to use the “this” keyword inside the callback function you can just specify it as the second argument. This will take us to the “this” keyword and “bind” objects so ill keep things simple for now.

The below example shows how we can use a class method as a callback for our array “map” method and since the class method uses the “this” keyword and we are calling the method from outside the class it will result in an error. For this, we can specify that the “this” refers to the Calculator instance “calc”.

class Calculator {
  total = 0;

  add(x = 0) {
    this.total += x;
  }

  subtract(x = 0) {
    this.total -= x;
  }

  divide(x = 0) {
    this.total /= x;
  }

  multiply(x = 0) {
    this.total *= x;
  }

  clear() {
    this.total = 0;
  }
}

const calc = new Calculator();
const numbers = [45, 12, 99, 2, 10, 78, 34];

numbers.forEach(calc.add, calc);

calc.total // 280
calc.clear();
numbers.map(function(n) {
  this.clear();
  this.add(n);
  this.multiply(3);
  this.divide(2);

  return this.total;
}, calc);
/* returns
[
  67.5, 18, 148.5,
  3, 15, 117, 51
]
*/

To learn more about function bind you can visit this page on MDN which does a good job explaining it.

Conclusion

The array data structure is simple, powerful, and rich. Every programming language provides its version of some or most of these methods to deal with an indexed list. JavaScript arrays are particularly fun to work with and one you should feel comfortable working with in general.

This article is part of a data structure series so check the page for more data structure posts and follow for any future ones. Thank you.

Illustration for “Array Data Structure in Javascript — Search, Sort, Filter, Map, & Reduce”

YouTube Channel: Before SemicolonWebsite: beforesemicolon.com

Share this article