Wie finde ich das erste Element eines Arrays, das einer booleschen Bedingung in JavaScript entspricht?

218

Ich frage mich, ob es eine bekannte, integrierte / elegante Möglichkeit gibt, das erste Element eines JS-Arrays zu finden, das einer bestimmten Bedingung entspricht. AC # -Äquivalent wäre List.Find .

Bisher habe ich eine Kombination mit zwei Funktionen wie diese verwendet:

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

Und dann kann ich verwenden:

var result = someArray.findFirst(isNotNullNorUndefined);

Aber da es in ECMAScript so viele Array-Methoden im funktionalen Stil gibt, gibt es vielleicht schon so etwas? Ich stelle mir vor, dass viele Leute ständig solche Dinge implementieren müssen ...

Jakub P.
quelle
6
Es gibt keine eingebaute Methode, aber es gibt Dienstprogrammbibliotheken, die sich dieser Funktionalität annähern, wie documentcloud.github.com/underscore
kinakuta
Underscore.js sieht wirklich sehr gut aus! Und es hat find (). Vielen Dank!
Jakub P.
1
Nur damit Sie wissen, können Sie dies reduzieren: return (typeof (o) !== 'undefined' && o !== null);auf das return o != null;. Sie sind genau gleichwertig.
Klippen des Wahnsinns
1
Gut zu wissen. Aber weißt du, ich misstraue den Zwangsoperatoren wie! = Oder ==. Ich würde es nicht einmal einfach testen können, da ich irgendwie überprüfen müsste, ob es keinen anderen Wert gibt, der auf diese Weise zu Null gezwungen wird ... :) Also, wie glücklich ich bin, eine Bibliothek zu haben, die dies erlaubt Ich möchte diese Funktion komplett entfernen ... :)
Jakub P.
Ich muss ehrlich sagen, dass dies eine ziemlich elegante Lösung ist. Das nächste, was ich finden kann, ist Array.prototype.some, das versucht herauszufinden, ob ein Element eine bestimmte Bedingung erfüllt, die Sie in Form einer Funktion an es übergeben. Leider gibt dies einen Booleschen Wert anstelle des Index oder des Elements zurück. Ich würde Ihre Lösung der Verwendung einer Bibliothek empfehlen, da Bibliotheken in der Regel viel größer sind und Dinge enthalten, die Sie nicht verwenden, und ich bevorzuge es, die Dinge leicht zu halten (da Sie möglicherweise nur die eine Funktion aus der bereitgestellten Suite verwenden).
Graham Robertson

Antworten:

217

Seit ES6 gibt es die native findMethode für Arrays. Dadurch wird die Aufzählung des Arrays beendet, sobald die erste Übereinstimmung gefunden wurde, und der Wert wird zurückgegeben.

const result = someArray.find(isNotNullNorUndefined);

Alte Antwort:

Ich muss eine Antwort posten, um diese filterVorschläge zu stoppen :-)

Da es in ECMAScript so viele Array-Methoden im funktionalen Stil gibt, gibt es vielleicht schon so etwas?

Sie können die someArray-Methode verwenden , um das Array zu iterieren, bis eine Bedingung erfüllt ist (und dann anzuhalten). Leider wird nur zurückgegeben, ob die Bedingung einmal erfüllt wurde, nicht von welchem ​​Element (oder an welchem ​​Index) sie erfüllt wurde. Also müssen wir es ein wenig ändern:

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
quelle
28
Ich kann nicht sagen, dass ich die Abneigung gegen filter () vollständig verstehe. Es mag langsamer sein, aber in Wirklichkeit; In den meisten Fällen, in denen dies wahrscheinlich verwendet wird, handelt es sich zunächst um eine kleine Liste, und die meisten JavaScript-Anwendungen sind nicht kompliziert genug, um sich wirklich um die Effizienz auf dieser Ebene zu sorgen. [] .filter (Test) .pop () oder [] .filter (Test) [0] sind einfach, nativ und lesbar. Natürlich spreche ich von geschäftlichen Apps oder Websites, die keine intensiven Apps wie Spiele sind.
Josh Mc
11
Durchlaufen Filterlösungen alle Arrays / Sammlungen? In diesem Fall ist die Filterung sehr ineffizient, da sie über das gesamte Array läuft, auch wenn der gefundene Wert der erste in der Sammlung ist. some()Auf der anderen Seite wird sofort zurückgegeben, was in fast allen Fällen viel schneller ist als das Filtern von Lösungen.
AlikElzin-Kilaka
@ AlikElzin-kilaka: Ja genau.
Bergi
15
@JoshMc sicher, aber es ist sinnvoll, nicht unentgeltlich ineffizient zu sein, wenn eine Lösung für ein einfaches Problem irgendwo wie Stack Overflow veröffentlicht wird. Viele Leute werden den Code von hier aus kopieren und in eine Dienstprogrammfunktion einfügen, und einige von ihnen werden diese Dienstprogrammfunktion irgendwann in einem Kontext verwenden, in dem Leistung wichtig ist, ohne über die Implementierung nachzudenken. Wenn Sie ihnen etwas gegeben haben, das zunächst eine effiziente Implementierung aufweist, haben Sie entweder ein Leistungsproblem gelöst, das sie sonst nicht hätten, oder ihnen eine Menge Entwicklungszeit gespart, um es zu diagnostizieren.
Mark Amery
1
@ SuperUberDuper: Nein. Siehe Mark Amerys Antwort unten.
Bergi
104

Ab ECMAScript 6 können Sie Array.prototype.finddies verwenden. Dies ist in Firefox (25.0), Chrome (45.0), Edge (12) und Safari (7.1) implementiert und funktioniert, jedoch nicht in Internet Explorer oder einer Reihe anderer alter oder ungewöhnlicher Plattformen .

Der folgende Ausdruck wird beispielsweise als ausgewertet 106.

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

Wenn Sie dies jetzt verwenden möchten, aber Unterstützung für IE oder andere nicht unterstützende Browser benötigen, können Sie einen Shim verwenden. Ich empfehle die es6-shim . MDN bietet auch eine Unterlegscheibe an, wenn Sie aus irgendeinem Grund nicht die gesamte es6-Unterlegscheibe in Ihr Projekt einfügen möchten. Für maximale Kompatibilität möchten Sie das es6-shim, da es im Gegensatz zur MDN-Version fehlerhafte native Implementierungen erkennt findund überschreibt (siehe den Kommentar, der mit "Umgehen von Fehlern in Array # find und Array # findIndex" beginnt, und die unmittelbar darauf folgenden Zeilen). .

Mark Amery
quelle
findist besser als filterda findstoppt sofort, wenn ein Element gefunden wird, das der Bedingung entspricht, während filteralle Elemente durchlaufen werden, um alle übereinstimmenden Elemente zu erhalten.
Anh Tran
59

Was ist mit der Verwendung von Filter und dem Abrufen des ersten Index aus dem resultierenden Array?

var result = someArray.filter(isNotNullNorUndefined)[0];
Phil Mander
quelle
6
Verwenden Sie weiterhin es5-Methoden. var result = someArray.filter (isNotNullNorUndefined) .shift ();
Someyoungideas
Obwohl ich selbst für die Antwort oben bei @Bergi gestimmt habe, denke ich, dass wir uns mit der ES6-Destrukturierung ein wenig verbessern können: var [result] = someArray.filter (isNotNullNorUndefined);
Nakul Manchanda
@someyoungideas Könnten Sie den Nutzen der Verwendung .shifthier erklären ?
Jakubiszon
3
@jakubiszon Der Vorteil der Verwendung shiftist, dass es " schick aussieht", aber tatsächlich verwirrender ist. Wer würde denken, dass das Aufrufen shift()ohne Argumente dasselbe ist wie das erste Element? Es ist unklar, IMO. Der Array-Zugriff ist ohnehin schneller: jsperf.com/array-access-vs-shift
Josh M.
Auch eckige Klammer - Notation ist für beide Objekte und Arrays in ES5 AFAIK verfügbar, nie eine Bevorzugung gesehen .shift()über [0]explizit wie folgt angegeben. Trotzdem ist es eine Alternative, die Sie wählen können oder nicht, ich würde mich jedoch daran halten [0].
SidOfc
15

Inzwischen sollte klar sein, dass JavaScript keine solche Lösung von Haus aus bietet. Hier sind die beiden nächsten Ableitungen, die nützlichsten zuerst:

  1. Array.prototype.some(fn)bietet das gewünschte Verhalten beim Stoppen, wenn eine Bedingung erfüllt ist, gibt jedoch nur zurück, ob ein Element vorhanden ist; Es ist nicht schwer, einige Tricks anzuwenden, wie die Lösung, die Bergis Antwort bietet .

  2. Array.prototype.filter(fn)[0]Dies ist ein großartiger Einzeiler, der jedoch am wenigsten effizient ist, da Sie N - 1Elemente wegwerfen , um das zu erhalten, was Sie benötigen.

Herkömmliche Suchmethoden in JavaScript zeichnen sich dadurch aus, dass der Index des gefundenen Elements anstelle des Elements selbst oder -1 zurückgegeben wird. Dadurch wird vermieden, dass ein Rückgabewert aus der Domäne aller möglichen Typen ausgewählt werden muss. Ein Index kann nur eine Zahl sein und negative Werte sind ungültig.

Beide oben genannten Lösungen unterstützen auch keine Offset-Suche, daher habe ich beschlossen, Folgendes zu schreiben:

(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
quelle
Sieht nach der umfassendsten Antwort aus. Können Sie Ihrer Antwort einen dritten Ansatz hinzufügen?
Mrusful
13

Zusammenfassung:

  • Um das erste Element in einem Array zu finden, das einer booleschen Bedingung entspricht, können wir das verwenden ES6 find()
  • find()befindet sich auf, Array.prototypeso dass es auf jedem Array verwendet werden kann.
  • find()nimmt einen Rückruf entgegen, bei dem eine booleanBedingung getestet wird. Die Funktion gibt den Wert zurück (nicht den Index!)

Beispiel:

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

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

console.log(found);   //  56

Willem van der Veen
quelle
8

Wenn Sie verwenden underscore.js, können Sie seine findund indexOfFunktionen verwenden, um genau das zu erhalten, was Sie wollen:

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

Dokumentation:

Matt Woelk
quelle
Verwenden Sie die Option underscorejs nur, wenn dies erforderlich ist, da das Laden einer Bibliothek nur dafür nicht wert ist
DannyFeliz
4

Ab ES 2015 ist Array.prototype.find()genau diese Funktionalität vorgesehen.

Für Browser, die diese Funktion nicht unterstützen, hat das Mozilla Developer Network eine Polyfüllung bereitgestellt (unten eingefügt):

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
quelle
2
foundElement = myArray[myArray.findIndex(element => //condition here)];
dotista2008
quelle
3
Ein reales Beispiel mit einer Bedingungsklausel und einigen erklärenden Worten würde Ihre Antwort wertvoller und verständlicher machen.
SaschaM78
0

Ich habe mich von mehreren Quellen im Internet inspirieren lassen, um die unten stehende Lösung zu finden. Wollte sowohl einige Standardwerte berücksichtigen als auch eine Möglichkeit bieten, jeden Eintrag für einen generischen Ansatz zu vergleichen, den dies löst.

Verwendung: (Wert "Sekunde" angeben)

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

Implementierung:

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
quelle
-2

In Javascript ist keine Funktion integriert, um diese Suche durchzuführen.

Wenn Sie jQuery verwenden, können Sie a jQuery.inArray(element,array).

PedroSena
quelle
Das würde auch funktionieren, obwohl ich wahrscheinlich mit Underscore gehen werde :)
Jakub P.
3
Dies erfüllt nicht die Ausgabe dessen, was der Fragesteller benötigt (benötigt das Element an einem Index, kein Boolescher Wert).
Graham Robertson
@GrahamRobertson $.inArraygibt keinen Booleschen Wert zurück, sondern (überraschenderweise!) Den Index des ersten übereinstimmenden Elements. Es macht jedoch immer noch nicht das, was das OP verlangt hat.
Mark Amery
-2

Eine weniger elegante Methode, mit der throwalle richtigen Fehlermeldungen (basierend auf Array.prototype.filter) angezeigt werden, die jedoch beim ersten Ergebnis nicht mehr wiederholt werden, ist

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

Dann sind Beispiele

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

Es funktioniert, indem es filtermit using endet throw.

Paul S.
quelle