Métodos para buscar en un Array

Figura
Figura. El método indexOf(valor) nos devuelve el índice donde se encuentra un valor a buscar.

Los métodos para realizar búsquedas en un Array se pueden agrupar en tres clases. Están los que devuelven un valor booleano según encuentren o no lo que buscan, como every(), some() e includes().

Por otro lado están los que devuelven el índice del valor a buscar, con los métodos findIndex(), indexOf() y lastIndexOf(). Si no lo encuentran devolverán -1.

Por último el método find() devuelve el valor que se está buscando según una condición en el callback. También funcionan con un callback los métodos some(), find() y findIndex().

El método includes() es de EcmaScript 2016 (ES7), pero ya los navegadores actuales más usados lo soportan.

arr.every(callback[, thisArg]])

ES5

El método every() comprueba si todos los elementos de un Array cumplen un criterio que se implementa en la función callback. En el siguiente código tenemos una funcion que evalúa si un valor no es un número con isNaN(), o mejor dicho, si no es convertible a un número. Vemos que aplicado al Array todos sus elementos son números. Usando una función flecha devolvemos si el nombre del constructor es Number, que es otra forma de hacer este test. Para el segundo Array devuelve falso, pues tiene un elemento que es un String.

function esNumero(v){
    return !isNaN(v);
}
let arr = [51, -12, 0, 3];
console.log(arr.every(esNumero)); // true
console.log(arr.every(v => v.constructor.name === "Number")); // true
let arr2 = [51, -12, 0, 3, "abc"];
console.log(arr2.every(v => v.constructor.name === "Number")); // false
    

Como con todos los métodos de Array que usan un argumento a modo de callback, ésta función tiene tres argumentos en este orden: el valor del elemento sobre el que estamos iterando, su índice y, finalmente, una referencia al Array. Lo abreviamos como v,i,a para recordarlo.

En el siguiente ejemplo usaremos los tres argumentos v,i,a para comprobar si un Array está ordenado. Por definición propia consideraremos que está desordenadado si algún elemento es nulo o indefinido, puesto que nos impedirá saber si hay un orden en el Array. Consideraremos que si hay un único elemento está ordenado. En el resto de casos comprobamos el orden creciente viendo que a[i] >= a[i-1].

function ordenado(v,i,a) {
    //Está ordenado si no contiene null o undefined.
    //Con el primer elemento devuelve cierto, pues puede ser un Array
    //con un sólo elemento y entonces se podría considerar ordenado. 
    //En otro caso comprueba el orden.
    return !(v === null || v === undefined) &&
           (i === 0 || a[i] >= a[i-1]);
}
console.log([8].every(ordenado)); // true
console.log([8, 19, 19, 23].every(ordenado)); // true
console.log([8, 7, 23].every(ordenado)); // false
console.log([8, undefined, 19, 23].every(ordenado)); // false
console.log([null].every(ordenado)); // false
console.log(["Juan", "Andrés", "Ana"].every(ordenado)); // false
console.log(["Juan", "Andrés", "Ana"].sort().every(ordenado)); // true
console.log([].every(ordenado)); // true
    

En el código observamos varios casos. Al Array de nombres de personas que está desordenado le pasamos el método sort(), viendo entonces que every() devuelve verdadero, como era de esperar.

El último caso de un Array vacío es interesante. Resulta que si aplicamos every() a un Array con elementos y devolvemos verdadero para cada uno, entonces el resultado final es verdadero. Y, obviamente, si devolvemos falso el resultado final será falso. Pero si el Array está vacío ambos resultados son verdaderos:

//En un Array no vacío devolvemos true o false: 
//el resultado es true o false respectivamente
console.log([1, 2].every(() => true)); // true
console.log([1, 2].every(() => false)); // false
//Pero para un Array vacío siempre será true
console.log([].every(() => true)); // true
console.log([].every(() => false)); // true
    

Es lo que se denomina una verdad vacua (vacous truth), pues un conjunto vacío satisface cualquier propiedad. Las sentencias si 4 es impar entonces 3=3 y si 4 es impar entonces 3≠3 son ambas dos verdades vacuas, pues su antecedente 4 es impar es falso y por tanto puede inferirse una cosa y la contraria.

La iteración sobre every() finaliza cuando se devuelve el primer falso en el callback. En cambio comprobar que es verdadero supone recorrer todo el Array. En el siguiente ejemplo comprobamos si todos los números son mayores que cero. La comprobación positiva conlleva recorrer todo el Array:

function fun(v,i) {
    cad += v + " ";
    return v > 0;
}
let arr = [16, -2, 7, 9];
let cad = "";
console.log(arr.every(fun)); // false
console.log(cad); // 16 -2
arr = [16, 2, 7, 9];
cad = "";
console.log(arr.every(fun)); // true
console.log(cad); // 16 2 7 9 
    

El método every puede aplicarse a los array-like. En este ejemplo propagamos los argumentos para pasarle every() y y comprobar que todos sus elementos son números. Si es así los sumamos con el método reduce().

function sumar(){
    let args = [...arguments];
    if (args.every(v => (v !== null) && (v !== undefined) &&
    (typeof v === "number"))){
        return args.reduce((p, v) => p+v, 0);
    } else {
        return "ERROR";
    }
}
console.log(sumar()); // 0
console.log(sumar(1, 2)); // 3
console.log(sumar(1, 0, 2)); // 3
console.log(sumar(1, "a", 2)); // "ERROR"
console.log(sumar(1, [2], 4)); // "ERROR"
console.log(sumar(1, null, 2)); // "ERROR"
    

Hay que recordar que siempre tenemos varias alternativas para aplicar un método a un array-like como arguments. En el siguiente código usamos cinco formas distintas:

// arguments no dispone del método every()
try {
    (function (){
        console.log(arguments.every(v => v > 0));
    })(1, 2);
} catch (e){
    console.log(e.message); // arguments.every is not a function
}
// Lo resolvemos con call() sobre el método del prototipo de Array
(function (){
    console.log(Array.prototype.every.call(arguments, v => v > 0));
})(1, 2); // true
// O aplicando call() sobre el método de un Array vacío
(function (){
    console.log([].every.call(arguments, v => v > 0));
})(1, 2); // true
// O propagando los argumentos
(function (...args){
    console.log(args.every(v => v > 0));
})(1, 2); // true
// O propagando arguments en un Array
(function (){
    console.log([...arguments].every(v => v > 0));
})(1, 2); // true
// O atando el método every() a una variable con bind()
(function (){
    let every = [].every.bind(arguments);
    console.log(every(v => v > 0));
})(1, 2); // true
    

arr.some(callback[, thisArg]])

ES5

El método some() comprueba si algún elemento en el Array cumple el test de la función callback. En este ejemplo con un Array de cadenas comprobamos si algún elemento es una cadena vacía. El método finaliza la iteración cuando encuentra el primer valor verdadero. Con every() obtener un valor verdadero suponía recorrer todo el Array. Y ahora con some() lo es para obtener un valor falso.

function esVacio(v){
    return v === "";
}
let arr = ["abc", "", "def"];
console.log(arr.some(esVacio)); // true
    

Con arr.some(v => v===valor) podemos saber si un valor existe en un Array. Eso haría lo mismo que el metodo includes(). Pueden haber más formas de saber si un Array tiene un valor, pero indudablemente la más simple de escribir es con includes().

let arr = [54, 48, 99, 75];
// Con indexOf()
console.log(arr.indexOf(99) > -1); // true
console.log(arr.indexOf(11) > -1); // false
// Con lastIndexOf()
console.log(arr.lastIndexOf(99) > -1); // true
console.log(arr.lastIndexOf(11) > -1); // false
// Con find()
console.log(arr.find(v => v===99) === 99); // true
console.log(arr.find(v => v===11) === 11); // false
// Con findIndex()
console.log(arr.findIndex(v => v===99) > -1); // true
console.log(arr.findIndex(v => v===11) > -1); // false
// Con filter()
console.log(arr.filter(v => v===99).length > 0); // true
console.log(arr.filter(v => v===11).length > 0); // false
// Con some()
console.log(arr.some(v => v===99)); // true
console.log(arr.some(v => v===11)); // false
// Con includes()
console.log(arr.includes(99)); // true
console.log(arr.includes(11)); // false
    

El callback tiene los argumentos v,i,a: valor e índice del elemento que se está comprobando y referencia al Array sobre el que se itera. En este ejemplo usamos los tres argumentos, utilizando dos veces some() para comprobar si hay al menos un elemento repetido en un Array. Hacemos uso del método slice() para hacer un recorte de la parte ya iterada con objeto de comprobar si el elemento cuestionado existe en ese trozo.

function estaRepetido(v, i, a) {
    return a.slice(0, i).some(vv => vv === v);
}
let arr1 = [2, 8, 5, 7, 1];
console.log(arr1.some(estaRepetido)); // false
let arr2 = [2, 8, 5, 8, 7, 1];
console.log(arr2.some(estaRepetido)); // true
    

El método some() puede aplicarse a los array-like. En este ejemplo propagamos un String en un Array para comprobar si entre las letras de un usuario hay caracteres que no se permitirían. En la función esCaracterProhibido() podríamos haber usado prohibidos.includes(v) en lugar de volver a usar otro some().

let prohibidos = [" ", ".", ",", ";"];
let esCaracterProhibido = v => prohibidos.some(vv => vv === v);
let user = "usuario general";
console.log([...user].some(esCaracterProhibido)); // true
user = "usuario.general";
console.log([...user].some(esCaracterProhibido)); // true
user = "usuario_general";
console.log([...user].some(esCaracterProhibido)); // false
    

El uso del argumento thisArg del método some() nos permitirá generalizar un ejemplo como el anterior, pudiendo usarlo para ver si hay algún carácter prohibido en el nombre de usuario o en la contraseña. Disponemos de una función que comprueba si el carácter está en el Array que traerá this a través de thisArg cuando llamemos a some(). De esta forma aprovechamos esa función para comprobar el usuario con el Array de caracteres prohibidosUser y el de contraseñas prohibidosPwd. Sólo por modificar algo pero con el mismo resultado, ahora los caracteres prohibidos están en un String, que luego propagamos a Array con [...this].

//Esto no puede ser una función flecha para tener
//acceso al this que viene desde thisArg en some()
function esProhibido(v){
    return [...this].includes(v);
}
let prohibidosUser = " .,;";
let prohibidosPwd = "-+*^";
//Comprobar usuario
let user = "usuario general";
console.log([...user].some(esProhibido, prohibidosUser)); // true
user = "usuario_general";
console.log([...user].some(esProhibido, prohibidosUser)); // false
//Comprobar contraseña
let password = "123*abc";
console.log([...password].some(esProhibido, prohibidosPwd)); // true
password = "123_abc";
console.log([...password].some(esProhibido, prohibidosPwd)); // false
    

arr.find(callback[, thisArg]])

ES6

El método find() encuentra un valor en un Array si ese valor cumple el callback. En otro caso devolverá undefined. En este ejemplo buscamos el primer número impar en un Array.

function primerImpar(v){
    return v % 2;
}
let arr = [2, 11, 8, 5, 44];
console.log(arr.find(primerImpar)); // 11
arr = [2, 8, 44];
console.log(arr.find(primerImpar)); // undefined
    

El callback tiene los argumentos v,i,a: valor, índice y Array. En el siguiente ejemplo los utilizamos para encontrar el primer impar al que le sigue otro impar. Y también el primer par al que le sigue otro par. El operador % es el resto de la división, por lo que para un impar v%2 será un número mayor que cero, y por tanto se coerciona true en una operación lógica. Mientras que para un par será cero y se coerciona a false, por lo que hemos de negarlo y tendremos que usar paréntesis !(v%2). Esto no es sino una forma de abreviar v%2>0 para impares y v%2===0 para pares.

let arr = [2, 15, 16, 4, 23, 7];
//Primer impar de dos seguidos
console.log(arr.find((v, i, a) => 
    v%2 && i<a.length-1 && a[i+1]%2)); // 23
//Primer par de dos seguidos
console.log(arr.find((v, i, a) =>
    !(v%2) && i<a.length-1 && !(a[i+1]%2))); // 16
    

Realmente en el código anterior el acceso al Array con el argumento del método (a) puede sustituirse por arr, pues dentro de esa función se tiene alcance a esa variable exterior. Sin embargo con el mismo ejemplo usando el método directamente sobre el Array no hay más forma de acceder al mismo que con el argumento del método:

//Primer impar de dos seguidos
console.log([2, 15, 16, 4, 23, 7].find((v, i, a) =>
    v%2 && i<a.length-1 && a[i+1]%2)); // 23
//Primer par de dos seguidos
console.log([2, 15, 16, 4, 23, 7].find((v, i, a) =>
    !(v%2) && i<a.length-1 && !(a[i+1]%2))); // 16
    

Pero además hay otra cosa. Cuando se ejecuta el método el Array queda congelado, de tal forma que cualquier variación en la variable exterior no se traspasa al argumento. Usando el ejemplo de impares ya dentro de la funcion anulamos arr y vemos que no afecta al resultado. Sin embargo si lo hacemos con el Array del argumento nos cursará un error. Por lo tanto tengamos o no acceso al Array exterior, siempre deberíamos usar la referencia del Array del argumento del método (el a de v,i,a).

let arr = [2, 15, 16, 4, 23, 7];
//Primer impar de dos seguidos
console.log(arr.find((v, i, a) => {
    arr = null;
    //si hacemos a=null nos dará error
    //Cannot read property 'length' of null
    return v%2 && i<a.length-1 && a[i+1]%2;
})); // 23
    

Como otros métodos también hay un argumento thisArg. En este ejemplo tenemos un primer Array con cadenas de texto que presentan números en formato con punto para separar miles y coma para decimales. En el segundo Array está el formato inglés, que es al revés. Vamos a encontrar el primer importe que supere 1000 indicando el formato adecuado. Dentro de la función ajustamos el String del importe según formato y luego lo comparamos con la base. Observe que el this es el que nos trae el argumento thisArg en la llamada de find().

let arr1 = ["230,45", "1.234,56", "6.335,11", "55,99"];
let arr2 = ["230.45", "1,234.56", "6,335.11", "55.99"];

function importeMayor (v){
    let valor = parseFloat((this.formato==="es") ? 
                v.replace(/\./g, "").replace(/,/g, ".") :
                v.replace(/,/g, ""));
    return valor > this.base;                
}

//En arr1 con formato "es" encontramos primer importe mayor 1000
console.log(arr1.find(importeMayor, 
    {base: 1000, formato: "es"})); "1.234,56"
//Y en arr2 con formato "en"
console.log(arr2.find(importeMayor,
    {base: 1000, formato: "en"})); "1,234.56"

//El resultado no es correcto si el formato no es adecuado
console.log(arr1.find(importeMayor, 
    {base: 1000, formato: "en"})); "230,45"
    

Cómo obtener el valor seleccionado en elementos input tipo radio

Un grupo de elementos HTML <input type="radio"> que tengan el mismo valor del atributo name forman una colección de botones de radio. Sólo uno de los botones de ese grupo puede estar pulsado, lo que se consigue dotándole del atributo checked. También es posible que ninguno de ellos tenga el atributo. En el ejemplo interactivo de este apartado tenemos el siguiente HTML:

<input type="radio" name="radio-find" value="OPCIÓN UNO" checked 
    id="radio-find1" /><label for="radio-find1">Opción 1</label>
<input type="radio" name="radio-find" value="OPCIÓN DOS" 
    id="radio-find2" /><label for="radio-find2">Opción 2</label>
<input type="radio" name="radio-find" value="OPCIÓN TRES" 
    id="radio-find3" /><label for="radio-find3">Opción 3</label>
    

El atributo id no es necesario para que funcione como un grupo de botones de radio. Su único propósito es servir de referencia para el atributo for del elemento <label> anexo, elemento que se resalta cuando seleccionamos un botón. Esto lo explicaremos más abajo.

La forma clásica de obtener el valor del grupo con JavaScript es con un bucle for. El bucle se rompe cuando encuentra un elemento seleccionado. Observe que si no hubiera ninguno en la variable opción tendríamos el valor inicial "?".

let opcion = "?";
let radios = document.getElementsByName("radio-find");
for (let i=0; i<radios.length; i++){
    if (radios[i].checked){
        opcion = radios[i].value;
        break;
    }
}
    

El bucle tambien podría ser un for of dado que getElementsByName() dispone de la función [Symbol.iterator]:

let opcion = "?";
let radios = document.getElementsByName("radio-find");
console.log(typeof radios[Symbol.iterator]); // "function"
for (let radio of radios){
    if (radio.checked){
        opcion = radio.value;
        break;
    }
}
    

Podríamos usar el método filter(), usando también el operador de propagación para convertir en un Array el grupo de elementos radio. Vea como podemos obtener el valor del elemento seleccionado en una sóla línea de código:

let opcion = "?";
opcion = [...document.getElementsByName("radio-find")].filter
         (v => v.checked)[0].value;
    

Si hay que usar un método de Array quizá es mejor utilizar find(), pues devuelve directamente el elemento HTML, mientras que filter() devuelve un Array:

let opcion = "?";
opcion = [...document.getElementsByName("radio-find")].find
         (v => v.checked).value;
    

Pero indudablemente la forma más simple de obtener un valor de un grupo de radios es con el método del DOM querySelector():

let opcion = "?";
opcion = document.querySelector("[name=radio-find]:checked").value;
    

El argumento de querySelector() es un selector CSS. Su uso es tal cual lo haríamos en CSS. En el código HTML que pusimos más arriba vimos que hay un <label> inmediatamente después de cada botón radio. Cuando uno de éstos está seleccionado la siguiente regla CSS lo resalta. Es el mismo selector que usamos para obtener el valor con querySelector().

[name=radio-find]:checked + label {
    background-color: yellow;
    color: blue;
}
    

Con estas tres últimas formas filter(), find() y querySelector() nos da un error si no hay ningún radio seleccionado. Habría que separar el código en dos partes, una para obtener el elemento y otra para ver si hay alguno y obtener en su caso el valor. Sería algo como esto:

let opcion = "?";
let radio = document.querySelector("[name=radio-find]:checked");  
if (radio) opcion = radio.value;
    

De todas formas no hay que olvidar que si damos una opción seleccionada en el HTML con el atributo checked, la única forma de deseleccionarlos todos es usando JavaScript, como en el ejemplo.

La última técnica pasa por incluir los radios en un elemento <form id="form-find">. No es necesario que los controles de formulario como <input>, <button>, <select> y otros estén dentro de un elemento formulario. El objetivo principal de un <form> es enviar valores al servidor. Cuando usamos los controles de formulario sólo para que el usuario interactúe con la página podemos usar o no el formulario. Si los ponemos dentro de un <form> podemos utilizar otra forma incluso más simple de obtener el valor de un grupo de radios:

let opcion = "?";
opcion = document.forms["form-find"]["radio-find"].value;
//También con
//opcion = document.getElementById("form-find")["radio-find"].value
    

El caso es que en toda página document.forms es una colección de formularios de la página. Por lo tanto podemos acceder a un determinado formulario por su identificador id. Cada formulario contiene una lista de los controles que incluye. Así accedemos al control ["radio-find"] de los botones de radio. Su valor es el del radio seleccionado. Con esta forma cuando no hay ningún radio seleccionado el valor es una cadena vacía.

No habiendo mayor problema de usar un elemento formulario cuyo destino no es enviar datos al servidor, si que debemos observar que ningun botón del mismo (tanto <input> como <button>) tenga el tipo submit, pues su acción enviaría el formulario al servidor. Además no hay que olvidar que el tipo por defecto de un <button> es submit. Por otro lado desde JavaScript también podría remitirse el formulario con document.forms["form-find"].submit().

No es que el problema sea grave, pues al no dotar del atributo action al formulario, la página de destino en el servidor sería la misma donde estamos. El envío se haría con el método GET, que es el usado por defecto cuando no se indica en el atributo method. Se produciría un reenvío de la misma página, observándose que ahora viene con una cadena de los parámetros GET.

Antes podíamos cancelar la acción de envío agregando un evento onsubmit al formulario que devolviera falso. Pero desde el hace algunos años los navegadores no permiten cancelar el envío de un formulario remitido con JavaScript con submit(), tal como vemos en esta página de Mozilla HTMLFormElement.submit(). Aunque las acciones que vengan de un botón con tipo submit aún si pueden cancelarse en el evento onsubmit. En todo caso es preferible siempre que sea posible destinar el elemento <form> sólo para enviar datos al servidor.

Ejemplo: Ejemplo uso de find() para obtener valor de opciones radio

Técnica a usar
let opcion = "?";
let radios = document.getElementsByName("radio-find");
for (let i=0; i<radios.length; i++){
    if (radios[i].checked){
        opcion = radios[i].value;
        break;
    }
}
        
Opciones de ejemplo

Tipo:

Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

arr.findIndex(callback[, thisArg]])

ES6

El método findIndex() encuentra el primer índice de un elemento que cumpla la condición del callback. En otro caso devuelve -1. Este código encuentra en el Array el índice del segundo elemento mayor que 55:

function callback(v){
    return v > 55;
}   
let arr = [33, 58, 15];
console.log(arr.findIndex(callback)); // 1
    

El método tiene el segundo argumento opcional thisArg. Además el callback tiene los tres argumentos v,i,a para el valor e índice actual y una referencia al Array. Vamos a usar todo esto en el siguiente ejemplo.

function buscar(v,i,a){
    if (this && this.detectarHuecos){
        arr = a.filter(v => v!==undefined);
        return v === undefined;
    }        
    return i > 0 && v !== a[i-1]+1;
}
let arr = [21, 22, 24, 25, , 27];
console.log(arr.findIndex(buscar)); // 2
console.log(arr); // [21, 22, 24, 25, , 27]
console.log(arr.findIndex(buscar, {detectarHuecos: true})); // 4
console.log(arr); // [21, 22, 24, 25, 27]
    

En el ejemplo anterior comprobamos si todos los números del Array son correlativos. Vemos que el índice 2 (valor 24) no es correlativo con el anterior. En thisArg pasamos un filtro para detectar huecos y, en general, valores undefined. En el ejemplo detecta el índice cuarto.

Si detectamos huecos en ese ejemplo los eliminamos en el Array original usando el método filter filtrando los elementos que no sean undefined. Esto lo ponemos sólo para evidenciar que dentro del callback podemos tener acceso al alcance de la variable arr, no pudiendo hacer el cambio en el argumento (a) pues no se reflejará en arr. Ese argumento es una referencia al Array cuyos valores se copian en el momento de llamar al método. Cualquier cambio posterior en arr no afectará a la referencia interna.

arr.includes(searchElement[, fromIndex]])

ES7

El método includes comprueba si un Array incluye un elemento con el valor del argumento. La comprobación se inicia por defecto desde la primera posición. O en su caso donde especifique el argumento opcional fromIndex. Este método, que es de la especificación ES7 publicada este año 2016, es soportado por algunos navegadores. Con some() también podríamos comprobar si un valor existe en un Array.

let arr = ["abc", 123, "xyz"];
console.log(arr.includes(123)); // true
console.log(arr.includes("def")); // false
console.log(arr.some(v => v==="xyz")); // true
    

Podemos aplicarlo a un array-like, como este objeto con propiedades numéricas correlativas y además con length:

let obj = {0: "xyz", 1: "abc", 2: "def", length: 3};
console.log([].includes.call(obj, "abc")); // true
    

El argumento fromIndex también puede ser negativo, en cuyo caso se tomará restando de length, o lo que es lo mismo, contando desde el final y hacia la derecha:

let arr = [10, 3, 25, 7, 11, 8];
//empieza en i=2 (25) y encuentra hacia la derecha el 7
console.log(arr.includes(7, 2)); // true
//empieza en i=6-2=4 (11) y NO encuentra hacia la derecha el 7
console.log(arr.includes(7, -2)); // false
    

Se usa una comparación estricta con === para buscar el valor. En una comparación no estricta sucede que "123" == 123 resulta veradero, mientras que en estricta "123" === 123 será falso.

let arr = [123];
console.log (arr[0] == "123"); // true
console.log (arr[0] === "123"); // false
console.log(arr.includes("123")); // false
    

arr.indexOf(searchElement[, fromIndex]])

ES5

El método indexOf() busca el índice de un elemento que coincida con el valor del argumento searchElement. Si no lo encuentra devuelve -1. La búsqueda empieza en el índice fromIndex. Si no se pasa este argumento opcional se considera cero. Este método funciona como includes() en todos los aspectos menos que ese devolvía un booleano y ahora indexOf() devuelve el índice si se encuentra o -1 en otro caso.

//     i:   0   1   2   3
let arr = [15, 20, 33, 35];
//Empieza buscando desde el índice 0 (en valor 15)
console.log(arr.indexOf(20)) // 1
//Empieza buscando desde el índice 2 (en valor 33)
console.log(arr.indexOf(20, 2)) // -1
    

El argumento fromIndex nos puede servir para buscar todas las coincidencias en el Array. En este ejemplo usamos un bucle do while para tener la condición de final de bucle después de aplicar el método:

let arr = ["X", "W", "X", "B"];
let index = -1, res = [];
do {
    index = arr.indexOf("X", index+1);
    if (index > -1) res.push(arr[index]);                                    
} while (index > -1)
console.log(res); // ["X", "X"]
    

arr.lastIndexOf(searchElement[, fromIndex]])

ES5

El método lastIndexOf() busca hacia atrás el índice de un elemento que coincida con el valor del argumento searchElement. Si no lo encuentra devuelve -1. La búsqueda empieza en el índice fromIndex. Si no se pasa este argumento opcional se considera el índice del último elemento, es decir, length-1. Este método funciona como indexOf() sólo que la búsqueda es desde el índice de fromIndex hacia el primer elemento. En este ejemplo comparamos ambos métodos:

//     i:   0   1   2   3
let arr = [15, 20, 33, 35];
//Desde i=0 hacia adelante
console.log(arr.indexOf(20)) // 1
//Desde i=3 hacia atrás
console.log(arr.lastIndexOf(20)) // 1
//Desde i=2 hacia adelante
console.log(arr.indexOf(20, 2)) // -1
//Desde i=2 hacia atrás
console.log(arr.lastIndexOf(20, 2)) // 1
    

Los métodos indexOf() y lastIndexOf() nos podrían servir para comprobar si un elemento está duplicado en un Array.

function estaDuplicado(arr, n){
    return arr.indexOf(n) !== arr.lastIndexOf(n);
}
let arr = [13, 87, 15, 46, 87, 24, 15, 102];
console.log(estaDuplicado(arr, 46)); // false
console.log(estaDuplicado(arr, 87)); // true
console.log(estaDuplicado(arr, 15)); // true
    

Con la misma idea, esta versión del ejemplo podría servirnos para eliminar duplicados en un Array:

function eliminarDuplicados(arr) {
    let iter = 0;
    do {
        let v = arr[iter++];
        //buscamos desde la posición actual, pues las
        //anteriores ya no estarán duplicadas al
        //eliminarlas por la izquierda con splice()
        let i = arr.indexOf(v, iter-1);
        let j = arr.lastIndexOf(v);
        if (i !== j) {
            //Eliminamos duplicado por la izquierda
            arr.splice(i, 1);
            iter--;
        }
    } while (iter < arr.length)
    return arr;
}
let arr = [13, 87, 7, 87, 15, 46, 87, 102, 102, 13, 15];
console.log(eliminarDuplicados(arr)); // [7, 46, 87, 102, 13, 15]
arr = [1,2,1,2,1,2,1,2,1,2];
console.log(eliminarDuplicados(arr)); // [1, 2]