¿Cómo encontrar el primer elemento de la matriz que coincida con una condición booleana en JavaScript?

218

Me pregunto si hay una forma conocida, integrada / elegante de encontrar el primer elemento de una matriz JS que coincida con una condición dada. AC # equivalente sería List.Find .

Hasta ahora he estado usando un combo de dos funciones como este:

// Returns the first element of an array that satisfies given predicate
Array.prototype.findFirst = function (predicateCallback) {
    if (typeof predicateCallback !== 'function') {
        return undefined;
    }

    for (var i = 0; i < arr.length; i++) {
        if (i in this && predicateCallback(this[i])) return this[i];
    }

    return undefined;
};

// Check if element is not undefined && not null
isNotNullNorUndefined = function (o) {
    return (typeof (o) !== 'undefined' && o !== null);
};

Y luego puedo usar:

var result = someArray.findFirst(isNotNullNorUndefined);

Pero dado que hay tantos métodos de matriz de estilo funcional en ECMAScript , ¿tal vez ya exista algo como esto? Me imagino que mucha gente tiene que implementar cosas como esta todo el tiempo ...

Jakub P.
fuente
66
No hay un método integrado, pero hay bibliotecas de utilidades que se aproximan a esta funcionalidad, como documentcloud.github.com/underscore
kinakuta
Underscore.js se ve muy bien! Y tiene find (). ¡Gracias!
Jakub P.
1
Para que lo sepas, puedes reducir esto: return (typeof (o) !== 'undefined' && o !== null);hasta esto return o != null;. Son exactamente equivalentes.
acantilados de locura
1
Bueno saber. Pero ya sabes, desconfío de los operadores coaccionadores como! = O ==. Ni siquiera podría probarlo fácilmente, ya que tendría que comprobar de alguna manera que no hay otro valor que sea obligado a nulo de esa manera ... :) Entonces, qué suerte tengo de tener una biblioteca que me permita eliminar esa función por completo ... :)
Jakub P.
Sinceramente, tengo que decir que esta es una solución bastante elegante. Lo más cercano que puedo encontrar es Array.prototype.some, que trata de encontrar si algún elemento satisface una condición dada que le pasas en forma de función. Desafortunadamente, esto devuelve un booleano en lugar del índice o el elemento. Recomendaría su solución sobre el uso de una biblioteca, ya que las bibliotecas tienden a ser mucho más grandes y contienen cosas que no usará y prefiero mantener las cosas livianas (ya que solo puede usar la única función del conjunto que proporciona).
Graham Robertson el

Respuestas:

217

Desde ES6 existe el findmétodo nativo para matrices; esto deja de enumerar la matriz una vez que encuentra la primera coincidencia y devuelve el valor.

const result = someArray.find(isNotNullNorUndefined);

Vieja respuesta:

Tengo que publicar una respuesta para detener estas filtersugerencias :-)

Dado que hay tantos métodos de matriz de estilo funcional en ECMAScript, ¿tal vez ya haya algo así como esto?

Puede usar el somemétodo Array para iterar la matriz hasta que se cumpla una condición (y luego se detenga). Desafortunadamente, solo devolverá si la condición se cumplió una vez, no por qué elemento (o en qué índice) se cumplió. Entonces tenemos que enmendarlo un poco:

function find(arr, test, ctx) {
    var result = null;
    arr.some(function(el, i) {
        return test.call(ctx, el, i, arr) ? ((result = el), true) : false;
    });
    return result;
}

var result = find(someArray, isNotNullNorUndefined);
Bergi
fuente
28
No puedo decir que entiendo completamente todo el disgusto dirigido a filter (). Puede ser más lento, pero en realidad; La mayoría de los casos en los que esto probablemente se usa, es una pequeña lista para comenzar, y la mayoría de las aplicaciones de JavaScript no son lo suficientemente complicadas como para preocuparse realmente por la eficiencia a este nivel. [] .filter (prueba) .pop () o [] .filter (prueba) [0] son ​​simples, nativos y legibles. Por supuesto, estoy hablando de aplicaciones empresariales o sitios web que no son aplicaciones intensivas como los juegos.
Josh Mc
11
¿Las soluciones de filtro atraviesan toda la matriz / colecciones? Si es así, el filtrado es muy ineficiente, ya que se ejecuta en toda la matriz, incluso si el valor encontrado es el primero de la colección. some()Por otro lado, regresa de inmediato, que es mucho más rápido en casi todos los casos que las soluciones de filtrado.
AlikElzin-kilaka
@ AlikElzin-kilaka: Sí, exactamente.
Bergi
15
@JoshMc seguro, pero tiene sentido no ser ineficiente de forma gratuita al publicar una solución a un problema simple en algún lugar como Stack Overflow. Mucha gente copiará y pegará el código desde aquí en una función de utilidad, y algunos de ellos, en algún momento, terminarán usando esa función de utilidad en un contexto donde el rendimiento es importante sin pensar en la implementación. Si les ha dado algo que tiene una implementación eficiente para empezar, ha resuelto un problema de rendimiento que de otro modo no tendrían, o les ha ahorrado un montón de tiempo de desarrollo para diagnosticarlo.
Mark Amery
1
@SuperUberDuper: No. Vea la respuesta de Mark Amery a continuación.
Bergi
104

A partir de ECMAScript 6, puede usarlo Array.prototype.findpara esto. Esto se implementa y funciona en Firefox (25.0), Chrome (45.0), Edge (12) y Safari (7.1), pero no en Internet Explorer o en un montón de otras plataformas antiguas o poco comunes .

Por ejemplo, la siguiente expresión se evalúa como 106.

[100,101,102,103,104,105,106,107,108,109].find(function (el) {
    return el > 105;
});

Si desea usar esto en este momento pero necesita soporte para IE u otros navegadores no compatibles, puede usar una cuña. Recomiendo el es6-shim . MDN también ofrece una cuña si, por alguna razón, no desea poner todo el es6-shim en su proyecto. Para obtener la máxima compatibilidad, desea el es6-shim, porque a diferencia de la versión MDN, detecta implementaciones nativas con errores findy las sobrescribe (vea el comentario que comienza "Solución de errores en Array # find y Array # findIndex" y las líneas que lo siguen inmediatamente) .

Mark Amery
fuente
findes mejor que filterya que se finddetiene inmediatamente cuando encuentra que un elemento coincide con la condición, mientras filterrecorre todos los elementos para proporcionar todos los elementos coincidentes.
Anh Tran
59

¿Qué pasa con el uso de filtro y obtener el primer índice de la matriz resultante?

var result = someArray.filter(isNotNullNorUndefined)[0];
Phil Mander
fuente
66
Sigue usando los métodos es5. resultado var = someArray.filter (isNotNullNorUndefined) .shift ();
someyoungideas
Aunque yo mismo voté por encontrar la respuesta anterior a @Bergi, creo que con la desestructuración de ES6 podemos mejorar un poco más arriba: var [resultado] = someArray.filter (isNotNullNorUndefined);
Nakul Manchanda
@someyoungideas, ¿podría explicar el beneficio de usar .shiftaquí?
jakubiszon
3
@jakubiszon El beneficio de usar shiftes que "parece inteligente" pero en realidad es más confuso. ¿Quién pensaría que llamar shift()sin argumentos sería lo mismo que tomar el primer elemento? No está claro IMO. El acceso a la matriz es más rápido de todos modos: jsperf.com/array-access-vs-shift
Josh M.
Además, notación de corchetes está disponible tanto para los objetos y las matrices en ES5 yo sepa, nunca había visto una preferencia de .shift()más [0]explícitamente declarado como este. A pesar de eso, es una alternativa que puedes elegir usar o no, aunque me quedaría [0].
SidOfc
15

Ya debería estar claro que JavaScript no ofrece tal solución de forma nativa; Aquí están los dos derivados más cercanos, los más útiles primero:

  1. Array.prototype.some(fn)ofrece el comportamiento deseado de detenerse cuando se cumple una condición, pero solo devuelve si un elemento está presente; No es difícil aplicar algunos trucos, como la solución ofrecida por la respuesta de Bergi .

  2. Array.prototype.filter(fn)[0]lo convierte en un gran one-liner pero es el menos eficiente, ya que tira los N - 1elementos solo para obtener lo que necesita.

Los métodos de búsqueda tradicionales en JavaScript se caracterizan por devolver el índice del elemento encontrado en lugar del elemento mismo o -1. Esto evita tener que elegir un valor de retorno del dominio de todos los tipos posibles; un índice solo puede ser un número y los valores negativos no son válidos.

Ambas soluciones anteriores tampoco admiten la búsqueda offset, por lo que he decidido escribir esto:

(function(ns) {
  ns.search = function(array, callback, offset) {
    var size = array.length;

    offset = offset || 0;
    if (offset >= size || offset <= -size) {
      return -1;
    } else if (offset < 0) {
      offset = size - offset;
    }

    while (offset < size) {
      if (callback(array[offset], offset, array)) {
        return offset;
      }
      ++offset;
    }
    return -1;
  };
}(this));

search([1, 2, NaN, 4], Number.isNaN); // 2
search([1, 2, 3, 4], Number.isNaN); // -1
search([1, NaN, 3, NaN], Number.isNaN, 2); // 3
Jack
fuente
Parece la respuesta más completa. ¿Puedes agregar un tercer enfoque en tu respuesta?
Mrusful
13

Resumen:

  • Para encontrar el primer elemento en una matriz que coincide con una condición booleana, podemos usar el ES6 find()
  • find()se encuentra en Array.prototypemodo que se puede usar en cada matriz.
  • find()toma una devolución de llamada donde booleanse prueba una condición. La función devuelve el valor (¡no el índice!)

Ejemplo:

const array = [4, 33, 8, 56, 23];

const found = array.find((element) => {
  return element > 50;
});

console.log(found);   //  56

Willem van der Veen
fuente
8

Si está usando underscore.js, puede usar sus funciones findy indexOffunciones para obtener exactamente lo que desea:

var index = _.indexOf(your_array, _.find(your_array, function (d) {
    return d === true;
}));

Documentación:

Matt Woelk
fuente
Use la opción underscorejs solo si es necesario porque cargar una biblioteca solo para esto simplemente no vale la pena
DannyFeliz
4

A partir de ES 2015, Array.prototype.find()proporciona esta funcionalidad exacta.

Para los navegadores que no admiten esta función, la Red de desarrolladores de Mozilla ha proporcionado un polyfill (pegado a continuación):

if (!Array.prototype.find) {
  Array.prototype.find = function(predicate) {
    if (this === null) {
      throw new TypeError('Array.prototype.find called on null or undefined');
    }
    if (typeof predicate !== 'function') {
      throw new TypeError('predicate must be a function');
    }
    var list = Object(this);
    var length = list.length >>> 0;
    var thisArg = arguments[1];
    var value;

    for (var i = 0; i < length; i++) {
      value = list[i];
      if (predicate.call(thisArg, value, i, list)) {
        return value;
      }
    }
    return undefined;
  };
}
Kevin Lee Garner
fuente
2
foundElement = myArray[myArray.findIndex(element => //condition here)];
dotista2008
fuente
3
Un ejemplo de la vida real con una cláusula de condición y algunas palabras explicativas haría que su respuesta sea más valiosa y comprensible.
SaschaM78
0

Me inspiré en múltiples fuentes en Internet para obtener la solución a continuación. Quería tener en cuenta algunos valores predeterminados y proporcionar una forma de comparar cada entrada para un enfoque genérico que esto resuelve.

Uso: (dando valor "Segundo")

var defaultItemValue = { id: -1, name: "Undefined" };
var containers: Container[] = [{ id: 1, name: "First" }, { id: 2, name: "Second" }];
GetContainer(2).name;

Implementación:

class Container {
    id: number;
    name: string;
}

public GetContainer(containerId: number): Container {
  var comparator = (item: Container): boolean => {
      return item.id == containerId;
    };
    return this.Get<Container>(this.containers, comparator, this.defaultItemValue);
  }

private Get<T>(array: T[], comparator: (item: T) => boolean, defaultValue: T): T {
  var found: T = null;
  array.some(function(element, index) {
    if (comparator(element)) {
      found = element;
      return true;
    }
  });

  if (!found) {
    found = defaultValue;
  }

  return found;
}
Henrik
fuente
-2

No hay una función incorporada en Javascript para realizar esta búsqueda.

Si está utilizando jQuery, puede hacer a jQuery.inArray(element,array).

PedroSena
fuente
Eso también funcionaría, aunque probablemente iré con el subrayado :)
Jakub P.
3
Esto no satisface la salida de lo que requiere el autor de la pregunta (necesita el elemento en algún índice, no un booleano).
Graham Robertson el
@GrahamRobertson $.inArrayno devuelve un valor booleano, (¡sorprendentemente!) Devuelve el índice del primer elemento coincidente. Sin embargo, todavía no hace lo que el OP solicitó.
Mark Amery
-2

Una forma menos elegante que tendrá throwtodos los mensajes de error correctos (basados ​​en Array.prototype.filter) pero dejará de iterar en el primer resultado es

function findFirst(arr, test, context) {
    var Result = function (v, i) {this.value = v; this.index = i;};
    try {
        Array.prototype.filter.call(arr, function (v, i, a) {
            if (test(v, i, a)) throw new Result(v, i);
        }, context);
    } catch (e) {
        if (e instanceof Result) return e;
        throw e;
    }
}

Entonces los ejemplos son

findFirst([-2, -1, 0, 1, 2, 3], function (e) {return e > 1 && e % 2;});
// Result {value: 3, index: 5}
findFirst([0, 1, 2, 3], 0);               // bad function param
// TypeError: number is not a function
findFirst(0, function () {return true;}); // bad arr param
// undefined
findFirst([1], function (e) {return 0;}); // no match
// undefined

Funciona al terminar filterusando throw.

Paul S.
fuente