5

In 2019, if I am handling an array of objects, with a length north of say 15000 and I need to find an index of an object by value, which of the following methods is going to be my best option performance-wise?

Six year old 'answer': In an array of objects, fastest way to find the index of an object whose attributes match a search

findIndex

array.findIndex(object => foo === object.id);

Array.prototype.map

array.map(object => object.id).indexOf(foo);
Friso Hoekstra
  • 845
  • 2
  • 9
  • 24
  • 7
    Without any doubt `findIndex` – Pranav C Balan Mar 19 '19 at 17:22
  • 4
    You could probably just write a jsperf for this. – tymeJV Mar 19 '19 at 17:22
  • 5
    Your second method makes **two** linear passes over your list, *and* it constructs a new array. – Pointy Mar 19 '19 at 17:23
  • 1
    The performance will depend on a number of things, including JavaScript engine, nature of the objects, nature of the function actually being called, etc. [See Eric Lippert's blog post on these kinds of questions](https://ericlippert.com/2012/12/17/performance-rant/). – Heretic Monkey Mar 19 '19 at 17:23
  • 2
    Not being flippant, but: try it and see. – MikeB Mar 19 '19 at 17:24
  • 3
    Also, there is [at least one answer to that question](https://stackoverflow.com/a/41260440/215552) that mentions `findIndex`. Just because a question is old does not mean that its answers hold no merit. – Heretic Monkey Mar 19 '19 at 17:25
  • 1
    Use either method and profile your code. – Felix Kling Mar 19 '19 at 17:26
  • "*Which one is faster?*" can be easily answered by just creating a benchmark to test them against each other. "*Why is one faster than the other?*" is a more interesting question and is better suited for this site. – p.s.w.g Mar 19 '19 at 17:30
  • 1
    Also consider if you can keep your 15000 elements sorted and use a binary search. see lodash `sortedIndexOf`: https://lodash.com/docs/4.17.11#sortedIndexOf – Doug Coburn Mar 19 '19 at 17:33
  • The Answers on your linked question are MUCH more recent than you imply! This questing does not bring anything new to the table that isn't already addressed and explained in answers already given to that question. Should this just be considered a "duplicate"? – SherylHohman Mar 19 '19 at 18:20

1 Answers1

19

Conceptually, these two snippets accomplish the same goal, but they go about it very different ways. To understand how the two solutions are different, let's first look at findIndex:

The findIndex method executes the callback function once for every array index 0..length-1 (inclusive) in the array until it finds one where callback returns a truthy value.
emphasis mine

In other words, it will stop as soon as it finds the item you're looking for. indexOf has a similar behavior, because it will return the index of the first item found.

On the other hand, looking at map:

map calls a provided callback function once for each element in an array, in order, and constructs a new array from the results.
emphasis mine

In other words, map doesn't care what item you're search for. Even if the item you're looking for is the first item in the array, map will still loop through 14999 other items to create a new array of id's. This means you'll end up doing quite a lot more work to achieve the same results, both in terms of temporal complexity (more time needed to loop through all those items) and spatial complexity (more memory needed to store that temporary array).

Side note: The above is not necessarily true if you use iterators / generators, which can sort of 'look ahead' in a sense to see if more work is needed. But I think this is outside the scope of this question.

However, if you're really concerned about performance, it's always a good idea to run a test for yourself. Here's a quick benchmark to demonstrate the relative performance of the two implementations. On my machine, I get findIndex: 0.023ms / map+indexOf: 0.572ms. Your mileage may vary somewhat:

var suite = new Benchmark.Suite();

const haystack = [];
let needle;

suite
  .add('findIndex', () => haystack.findIndex(o => o.id === needle))
  .add('map+indexOf', () => haystack.map(o => o.id).indexOf(needle))
  .on('start', () => {
    for (let i = 0; i < 15000; i++) {
      haystack.push({
        id: Math.random().toString(36).substring(7)
      });
    }
    console.log('Starting test.');
  })
  .on('cycle', () => {
    needle = haystack[Math.floor(Math.random() * haystack.length)].id;
  })
  .on('complete', () => {
    console.log('Test results (lower is better):')
    suite.forEach((bench) => {
      console.log(`  ${bench.name}: ${(bench.stats.mean * 1000).toFixed(3)}ms`);
    });
  })
  .run({
    'async': true
  });
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/platform/1.3.5/platform.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/benchmark/2.1.4/benchmark.min.js"></script>
p.s.w.g
  • 146,324
  • 30
  • 291
  • 331