Come trovare il primo elemento dell'array che corrisponde a una condizione booleana in JavaScript?


246

Mi chiedo se esista un modo noto, integrato / elegante per trovare il primo elemento di un array JS che corrisponde a una determinata condizione. L'equivalente AC # sarebbe List.Find .

Finora ho utilizzato una combinazione a due funzioni come questa:

// 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);
};

E poi posso usare:

var result = someArray.findFirst(isNotNullNorUndefined);

Ma poiché ci sono così tanti metodi di array in stile funzionale in ECMAScript , forse c'è già qualcosa di simile? Immagino che molte persone debbano implementare cose del genere tutto il tempo ...


6
Non esiste un metodo integrato, ma ci sono librerie di utilità che approssimano questa funzionalità come documentcloud.github.com/underscore
kinakuta

Underscore.js sembra davvero molto carino! E ha find (). Grazie!
Jakub P.

1
Solo così sai, puoi ridurre questo: return (typeof (o) !== 'undefined' && o !== null);fino a questo return o != null;. Sono esattamente equivalenti.
scogliere della follia

1
Buono a sapersi. Ma sai, diffido degli operatori coercitivi come! = O ==. Non sarei nemmeno in grado di testarlo facilmente, poiché avrei bisogno di verificare in qualche modo che non ci sia nessun altro valore che sia costretto a nullo in quel modo ... :) Quindi quanto sono fortunato ad avere una libreria che me di rimuovere del tutto quella funzione ... :)
Jakub P.

Onestamente devo dire che questa è una soluzione abbastanza elegante. La cosa più vicina che riesco a trovare è Array.prototype.some che cerca di trovare se qualche elemento soddisfa una data condizione gli passi sotto forma di una funzione. Sfortunatamente, questo restituisce un valore booleano invece dell'indice o dell'elemento. Consiglierei la tua soluzione rispetto all'uso di una libreria poiché le librerie tendono ad essere molto più grandi e contengono cose che non userete e preferisco mantenere le cose leggere (dato che potresti usare solo l'unica funzione della suite che fornisce).
Graham Robertson

Risposte:


258

A partire da ES6 esiste il findmetodo nativo per gli array; questo interrompe l'enumerazione dell'array una volta che trova la prima corrispondenza e restituisce il valore.

const result = someArray.find(isNotNullNorUndefined);

Vecchia risposta:

Devo pubblicare una risposta per interrompere questi filtersuggerimenti :-)

poiché ci sono così tanti metodi di array in stile funzionale in ECMAScript, forse c'è già qualcosa di simile?

È possibile utilizzare il somemetodo Array per iterare l'array fino a quando non viene soddisfatta una condizione (e quindi interrompere). Sfortunatamente restituirà solo se la condizione è stata soddisfatta una volta, non da quale elemento (o in quale indice) è stata soddisfatta. Quindi dobbiamo modificarlo un po ':

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);

30
Non posso dire di comprendere appieno tutta l'avversione diretta a filter (). Potrebbe essere più lento, ma in realtà; nella maggior parte dei casi in cui viene probabilmente utilizzato, è un piccolo elenco per cominciare e la maggior parte delle applicazioni JavaScript non è abbastanza complicata da preoccuparsi davvero dell'efficienza a questo livello. [] .filter (test) .pop () o [] .filter (test) [0] sono semplici, nativi e leggibili. Ovviamente sto parlando di app aziendali o siti Web non intensivi come i giochi.
Josh Mc,

12
Le soluzioni di filtro attraversano tutti gli array / raccolte? In tal caso, il filtraggio è molto inefficiente, perché viene eseguito su tutto l'array anche se il valore trovato è il primo della raccolta. some()d'altra parte, ritorna immediatamente, che è molto più veloce in quasi tutti i casi rispetto alle soluzioni di filtraggio.
AlikElzin-kilaka

@ AlikElzin-kilaka: Sì, esattamente.
Bergi

16
@JoshMc certo, ma ha senso non essere gratuitamente inefficiente quando si pubblica una soluzione a un semplice problema da qualche parte come Stack Overflow. Molte persone copieranno e incolleranno il codice da qui in una funzione di utilità e alcuni di loro, ad un certo punto, finiranno per utilizzare quella funzione di utilità in un contesto in cui le prestazioni contano senza pensare all'implementazione. Se hai dato loro qualcosa che ha un'implementazione efficiente per cominciare, hai risolto un problema di prestazioni che altrimenti non avrebbero avuto, o hai risparmiato loro un po 'di tempo per lo sviluppo nella diagnosi.
Mark Amery

1
@SuperUberDuper: No. Vedi la risposta di Mark Amery di seguito.
Bergi

114

A partire da ECMAScript 6, puoi usare Array.prototype.findper questo. Questo è implementato e funziona in Firefox (25.0), Chrome (45.0), Edge (12) e Safari (7.1), ma non in Internet Explorer o in un mucchio di altre piattaforme vecchie o non comuni .

Ad esempio, di xseguito è 106:

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

Se vuoi usarlo adesso ma hai bisogno del supporto per IE o altri browser non supportati, puoi usare uno shim. Raccomando es6-shim . MDN offre anche uno shim se per qualche motivo non vuoi inserire l'intero es6-shim nel tuo progetto. Per la massima compatibilità si desidera es6-shim, perché a differenza della versione MDN rileva le implementazioni native difettose finde le sovrascrive (vedere il commento che inizia "Work around bugs in Array # find e Array # findIndex" e le righe immediatamente successive) .


findè meglio di filterpoiché si findferma immediatamente quando trova un elemento che corrisponde alla condizione, mentre filterscorre tutti gli elementi per fornire tutti gli elementi corrispondenti.
Anh Tran

64

Che ne dici di usare il filtro e ottenere il primo indice dall'array risultante?

var result = someArray.filter(isNotNullNorUndefined)[0];

7
Continua a utilizzare i metodi es5. var result = someArray.filter (isNotNullNorUndefined) .shift ();
someyoungideas

Anche se io stesso ho votato per trovare la risposta sopra @Bergi, penso che con la destrutturazione ES6 possiamo migliorare un po 'sopra: var [risultato] = someArray.filter (isNotNullNorUndefined);
Nakul Manchanda

@someyoungideas potresti spiegare il vantaggio di usare .shiftqui?
jakubiszon

4
@jakubiszon Il vantaggio dell'utilizzo shiftè che "sembra intelligente" ma in realtà crea più confusione. Chi penserebbe che chiamare shift()senza argomenti sarebbe come prendere il primo elemento? Non è chiaro IMO. L'accesso agli array è comunque più rapido: jsperf.com/array-access-vs-shift
Josh M.

Inoltre, la notazione tra parentesi quadre è disponibile sia per gli oggetti che per gli array in ES5 AFAIK, mai visto una preferenza di .shift()oltre [0]esplicitamente dichiarato come questo. Nonostante ciò, è un'alternativa che puoi scegliere di usare o meno, ma rimango fedele [0].
SidOfc

16

Sommario:

  • Per trovare il primo elemento in un array che corrisponde a una condizione booleana possiamo usare il ES6 find()
  • find()si trova su in Array.prototypemodo che possa essere utilizzato su ogni array.
  • find()accetta una richiamata quando booleanviene verificata una condizione. La funzione restituisce il valore (non l'indice!)

Esempio:

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

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

console.log(found);   //  56


Se vuoi l'indice invece del valore, usafindIndex
Flimm

15

Dovrebbe essere chiaro a questo punto che JavaScript non offre tale soluzione in modo nativo; ecco le due derivate più vicine, la prima più utile:

  1. Array.prototype.some(fn)offre il comportamento desiderato di fermarsi quando una condizione è soddisfatta, ma restituisce solo se un elemento è presente; non è difficile applicare qualche trucco, come la soluzione offerta dalla risposta di Bergi .

  2. Array.prototype.filter(fn)[0]è un ottimo one-liner ma è il meno efficiente, perché butti via gli N - 1elementi solo per ottenere ciò di cui hai bisogno.

I metodi di ricerca tradizionali in JavaScript sono caratterizzati dalla restituzione dell'indice dell'elemento trovato invece dell'elemento stesso o -1. Ciò evita di dover scegliere un valore di ritorno dal dominio di tutti i tipi possibili; un indice può essere solo un numero e i valori negativi non sono validi.

Entrambe le soluzioni sopra non supportano nemmeno la ricerca offset, quindi ho deciso di scrivere questo:

(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

Sembra la risposta più completa. Puoi aggiungere un terzo approccio nella tua risposta?
Mrusful


4

A partire da ES 2015, Array.prototype.find()fornisce questa funzionalità esatta.

Per i browser che non supportano questa funzione, Mozilla Developer Network ha fornito un polyfill (incollato di seguito):

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;
  };
}


2
foundElement = myArray[myArray.findIndex(element => //condition here)];

3
Un esempio di vita reale con una clausola di condizione e alcune parole esplicative renderebbe la tua risposta più preziosa e comprensibile.
SaschaM78

0

Ho tratto ispirazione da più fonti su Internet per ricavare la soluzione di seguito. Volevo prendere in considerazione sia alcuni valori predefiniti sia fornire un modo per confrontare ogni voce per un approccio generico che questo risolve.

Utilizzo: (dando valore "Secondo")

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

Implementazione:

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;
}

-2

Non esiste una funzione incorporata in Javascript per eseguire questa ricerca.

Se stai usando jQuery potresti fare un file jQuery.inArray(element,array).


Anche questo funzionerebbe, anche se probabilmente andrò con Underscore :)
Jakub P.

3
Questo non soddisfa l'output di ciò che richiede il richiedente (necessita dell'elemento in un indice, non in un booleano).
Graham Robertson

@GrahamRobertson $.inArraynon restituisce un valore booleano, ma (sorprendentemente!) Restituisce l'indice del primo elemento corrispondente. Tuttavia, continua a non fare ciò che l'OP ha chiesto.
Mark Amery

-2

Un modo meno elegante che mostrerà throwtutti i messaggi di errore giusti (basati su Array.prototype.filter) ma smetterà di iterare sul primo risultato è

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;
    }
}

Allora gli esempi sono

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

Funziona terminando filterusando throw.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.