Searching and Testing

Mental model: Ask for the element with find, for its position with findIndex, and for a yes or no answer with some and every.

Level: beginner · about 11 minutes

const users = [
  { id: 1, name: 'ada', admin: false },
  { id: 2, name: 'grace', admin: true },
];

console.log(users.find((u) => u.admin));      // { id: 2, name: 'grace', admin: true }
console.log(users.findIndex((u) => u.admin)); // 1
console.log(users.some((u) => u.admin));      // true
console.log(users.filter((u) => u.admin));    // [ { id: 2, ... } ], an array of one

Same question, four different answers.

Pick the method that matches the question you are actually asking. filter always hands you an array, so users.filter(fn)[0] works but says the wrong thing and keeps scanning after it has an answer. find stops at the first match and hands you the element.

MethodTakesReturnsNot found
find(fn)a predicatethe first matching elementundefined
findIndex(fn)a predicateits index-1
findLast(fn)a predicatethe last matching elementundefined
findLastIndex(fn)a predicateits index-1
includes(v)a valuetrue or falsefalse
indexOf(v)a valueits index-1
some(fn)a predicatetrue if any matchfalse
every(fn)a predicatetrue if all matchtrue on an empty array

Searching from the end

const readings = [12, 40, 8, 40, 3];

console.log(readings.findIndex((n) => n === 40));     // 1, first from the left
console.log(readings.findLastIndex((n) => n === 40)); // 3, first from the right
console.log(readings.findLast((n) => n > 10));        // 40
console.log(readings.lastIndexOf(40));                // 3, value search, not predicate

some and every: one boolean, short-circuited

const scores = [70, 85, 90];

console.log(scores.some((n) => n < 75));   // true, stops at 70
console.log(scores.every((n) => n >= 70)); // true, has to check all three
console.log([].some((n) => true));         // false, nothing to find
console.log([].every((n) => false));       // true, "all zero of them passed"

The NaN search caveat

const values = [1, NaN, 3];

console.log(values.indexOf(NaN));   // -1, cannot find it
console.log(values.includes(NaN));  // true, finds it
console.log(NaN === NaN);           // false, the reason indexOf fails

The same array, two search methods, two different answers.

indexOf compares with strict equality, and NaN === NaN is false, so NaN is invisible to it. includes uses SameValueZero, which treats NaN as equal to itself while still treating 0 and -0 as the same. Object.is is stricter again: it separates 0 from -0.

console.log([0].includes(-0));            // true, SameValueZero
console.log([0].indexOf(-0));             // 0, strict equality agrees here
console.log(Object.is(0, -0));            // false, the strictest comparison
console.log([1, NaN].findIndex((v) => Object.is(v, NaN))); // 1, search NaN by index
is it there?
includes
where is it?
indexOf for a value, findIndex for a condition
give me the object
find
any of them?
some
all of them?
every
searching for `NaN`?
includes, never indexOf
const ids = [0, 5, 9];
if (ids.indexOf(0)) console.log('found 0');
else console.log('did not find 0');

indexOf(0) returns the index 0, which is falsy, so the else branch runs even though the value is present. Every truthiness check on an index is a bug waiting for index 0. Use ids.includes(0).

Try it yourself

Ask the right question

const logins = [
  { user: 'ada', ok: true },
  { user: 'alan', ok: false },
  { user: 'grace', ok: true },
  { user: 'alan', ok: false },
];

console.log(logins.find((l) => !l.ok));           // first failure
console.log(logins.findIndex((l) => !l.ok));      // where it was
console.log(logins.some((l) => !l.ok));           // any failures at all?
console.log(logins.every((l) => l.user.length > 2)); // all names long enough?
console.log(logins.map((l) => l.user).includes('grace')); // is grace here?

Add a findLast that gets the most recent failed login. Then rewrite the some call as a find and compare what you get back.

Exercises

An indexOf that can find NaN

Write strictIndexOf(arr, value) that returns the index of the first element that is the same value as value, or -1 if there is none. It must find NaN, and it must treat 0 and -0 as different.

Check yourself

What does this log?
-1 true — indexOf uses strict equality and NaN === NaN is false, so it reports -1. includes uses SameValueZero, which does consider NaN equal to itself, so it returns true.
You need the user object whose id is 42, from an array of thousands. Which is best?
users.find((u) => u.id === 42) — find stops at the first match and returns the element itself. The filter version scans the whole array and then throws away all but one result. includes and indexOf compare against whole elements, not against a property.
What does [].every((x) => x > 100) return?
true — There is no element that fails the test, so the claim holds trivially. If "no items" should be a failure in your code, test the length separately rather than expecting every to do it.

Common mistakes

  • Using an index result as a boolean. -1 is truthy and 0 is falsy.
  • Reaching for filter(...)[0] when you meant find.
  • Trying to locate NaN with indexOf, which always reports -1.

Takeaways

  • find returns the element, findIndex the position, some/every a boolean.
  • findLast and findLastIndex search from the right without reversing the array.
  • includes uses SameValueZero, so it finds NaN; indexOf uses ===, so it cannot.
  • every on an empty array is true, and some on an empty array is false.