Jak znaleźć pierwszy element tablicy pasujący do warunku logicznego w JavaScript?

220

Zastanawiam się, czy istnieje znany, wbudowany / elegancki sposób na znalezienie pierwszego elementu tablicy JS pasującego do danego warunku. Odpowiednikiem AC # będzie List.Find .

Do tej pory korzystałem z dwufunkcyjnej kombinacji:

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

A potem mogę użyć:

var result = someArray.findFirst(isNotNullNorUndefined);

Ale skoro w ECMAScript jest wiele metod tablic funkcjonalnych , być może istnieje już coś takiego? Wyobrażam sobie, że wiele osób musi wdrażać takie rzeczy przez cały czas ...

Jakub P.
źródło
6
Nie ma wbudowanej metody, ale istnieją biblioteki narzędziowe zbliżone do tej funkcjonalności, takie jak documentcloud.github.com/underscore
kinakuta
Underscore.js naprawdę wygląda bardzo ładnie! I ma find (). Dzięki!
Jakub P.
1
Po prostu wiesz, możesz to zredukować: return (typeof (o) !== 'undefined' && o !== null);do tego return o != null;. Są dokładnie równoważne.
klify szaleństwa
1
Dobrze wiedzieć. Ale wiesz, nie ufam operatorom wymuszającym, takim jak! = Lub ==. Nie byłbym nawet w stanie łatwo go przetestować, ponieważ musiałbym w jakiś sposób sprawdzić, czy nie ma żadnej innej wartości, która byłaby zmuszona do zerowania w ten sposób ... :) Więc jak mam szczęście, że mam bibliotekę, która pozwoliła ja całkowicie usunę tę funkcję ... :)
Jakub P.
Muszę szczerze powiedzieć, że to dość eleganckie rozwiązanie. Najbliższą rzeczą, jaką mogę znaleźć, jest Array.prototype.some, który próbuje znaleźć, czy jakiś element spełnia dany warunek, który mu przekazujesz w postaci funkcji. Niestety zwraca wartość logiczną zamiast indeksu lub elementu. Poleciłbym twoje rozwiązanie zamiast korzystania z biblioteki, ponieważ biblioteki są zwykle znacznie większe i zawierają rzeczy, których nie będziesz używać, a ja wolę trzymać rzeczy lekkie (ponieważ możesz użyć tylko jednej funkcji z pakietu, który ona zapewnia).
Graham Robertson

Odpowiedzi:

219

Od wersji ES6 istnieje natywna findmetoda tablic; przestaje to wyliczać tablicę, gdy znajdzie ona pierwsze dopasowanie i zwraca wartość.

const result = someArray.find(isNotNullNorUndefined);

Stara odpowiedź:

Muszę opublikować odpowiedź, aby zatrzymać te filtersugestie :-)

skoro w ECMAScript jest wiele metod tablic funkcjonalnych, być może istnieje już coś takiego?

Możesz użyć somemetody Array do iteracji tablicy, dopóki warunek nie zostanie spełniony (a następnie zatrzymany). Niestety zwróci tylko to, czy warunek został spełniony tylko raz, a nie przez który element (lub przy jakim indeksie) został spełniony. Musimy więc trochę to zmienić:

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
źródło
28
Nie mogę powiedzieć, że całkowicie rozumiem wszystkie niechęci skierowane na filter (). Może być wolniejszy, ale w rzeczywistości; w większości przypadków, gdy jest to prawdopodobnie używane, jest to mała lista na początek, a większość aplikacji JavaScript nie jest wystarczająco skomplikowana, aby naprawdę martwić się wydajnością na tym poziomie. [] .filter (test) .pop () lub [] .filter (test) [0] są proste, natywne i czytelne. Oczywiście mówię o aplikacjach biznesowych lub witrynach mało intensywnych, takich jak gry.
Josh Mc
11
Czy rozwiązania filtrów przechodzą przez wszystkie tablice / kolekcje? Jeśli tak, filtrowanie jest bardzo nieefektywne, ponieważ działa na całej tablicy, nawet jeśli znaleziona wartość jest pierwszą w kolekcji. some()z drugiej strony wraca natychmiast, co jest prawie szybsze w prawie wszystkich przypadkach niż rozwiązania filtrujące.
AlikElzin-kilaka
@ AlikElzin-kilaka: Tak, dokładnie.
Bergi,
15
@JoshMc na pewno, ale sensownie jest nie być nieskutecznym, gdy publikujesz rozwiązanie prostego problemu, takiego jak przepełnienie stosu. Wiele osób skopiuje stąd i wklei kod do funkcji narzędzia, a niektórzy z nich w pewnym momencie skorzystają z tej funkcji narzędzia w kontekście, w którym wydajność ma znaczenie bez myślenia o implementacji. Jeśli dałeś im coś, co na początek ma wydajną implementację, albo rozwiązałeś problem z wydajnością, którego inaczej by nie mieli, albo zaoszczędziłeś sporo czasu na diagnozowanie go.
Mark Amery
1
@SuperUberDuper: Nie. Zobacz odpowiedź Marka Amery poniżej.
Bergi,
104

Począwszy od ECMAScript 6, możesz Array.prototype.finddo tego użyć . Jest to zaimplementowane i działa w Firefox (25.0), Chrome (45.0), Edge (12) i Safari (7.1), ale nie w Internet Explorerze ani innych starych lub rzadkich platformach .

Na przykład poniższe wyrażenie ma wartość 106.

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

Jeśli chcesz teraz tego używać, ale potrzebujesz pomocy dla IE lub innych nieobsługujących przeglądarek, możesz użyć podkładki dystansowej. Polecam podkładkę es6 . MDN oferuje również podkładkę, jeśli z jakiegoś powodu nie chcesz wkładać całego podkładu es6 do swojego projektu. Aby uzyskać maksymalną kompatybilność, potrzebujesz podkładki es6-shim, ponieważ w przeciwieństwie do wersji MDN wykrywa ona błędne natywne implementacje findi zastępuje je (patrz komentarz rozpoczynający się od „Obejścia błędów w Array # find i Array # findIndex” oraz wiersze bezpośrednio po nim) .

Mark Amery
źródło
findjest lepszy niż, filterponieważ findzatrzymuje się natychmiast, gdy znajdzie element spełniający warunek, podczas gdy filterprzechodzi przez wszystkie elementy, aby dać wszystkie dopasowane elementy.
Anh Tran,
59

A co z użyciem filtra i uzyskaniem pierwszego indeksu z wynikowej tablicy?

var result = someArray.filter(isNotNullNorUndefined)[0];
Phil Mander
źródło
6
Nadal używaj metod es5. var result = someArray.filter (isNotNullNorUndefined) .shift ();
someyoungideas
Chociaż sam głosowałem za znalezieniem odpowiedzi powyżej @Bergi, myślę, że dzięki restrukturyzacji ES6 możemy nieco poprawić: var [wynik] = someArray.filter (isNotNullNorUndefined);
Nakul Manchanda
@someyoungideas, czy możesz wyjaśnić korzyści płynące z korzystania .shifttutaj?
jakubiszon
3
@jakubiszon Zaletą używania shiftjest to, że „wygląda elegancko”, ale w rzeczywistości jest bardziej mylące. Kto by pomyślał, że sprawdzenie shift()bez argumentów byłoby tym samym, co wzięcie pierwszego elementu? Niejasne jest IMO. Dostęp do tablicy jest i tak szybszy: jsperf.com/array-access-vs-shift
Josh M.
Również notacja w nawiasach kwadratowych jest dostępna zarówno dla obiektów, jak i tablic w ES5 AFAIK, nigdy nie widziała preferencji .shift()nad [0]jawnie podanym w ten sposób. Mimo to jest to alternatywa, z której możesz skorzystać, czy nie, trzymałbym się [0]jednak.
SidOfc
15

Do tej pory powinno być jasne, że JavaScript nie oferuje takiego rozwiązania natywnie; oto dwie najbliższe pochodne, najbardziej przydatne jako pierwsze:

  1. Array.prototype.some(fn)oferuje pożądane zachowanie zatrzymania po spełnieniu warunku, ale zwraca tylko to, czy element jest obecny; nietrudno zastosować pewne sztuczki, takie jak rozwiązanie oferowane przez odpowiedź Bergi .

  2. Array.prototype.filter(fn)[0]tworzy świetną jednowarstwową, ale jest najmniej wydajna, ponieważ wyrzucasz N - 1elementy tylko po to, aby uzyskać to, czego potrzebujesz.

Tradycyjne metody wyszukiwania w JavaScript charakteryzują się zwracaniem indeksu znalezionego elementu zamiast samego elementu lub -1. Pozwala to uniknąć konieczności wybierania wartości zwracanej z domeny wszystkich możliwych typów; indeks może być tylko liczbą, a wartości ujemne są nieprawidłowe.

Oba powyższe rozwiązania nie obsługują również wyszukiwania offsetowego, więc postanowiłem napisać to:

(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
Jacek
źródło
Wygląda na najbardziej wyczerpującą odpowiedź. Czy możesz dodać trzecie podejście w swojej odpowiedzi?
Mrusful
13

Podsumowanie:

  • Aby znaleźć pierwszy element w tablicy, który pasuje do warunku logicznego, możemy użyć ES6 find()
  • find()znajduje się na, Array.prototypewięc można go używać w każdej tablicy.
  • find()odbiera oddzwonienie, gdy booleantestowany jest warunek. Funkcja zwraca wartość (nie indeks!)

Przykład:

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

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

console.log(found);   //  56

Willem van der Veen
źródło
8

Jeśli używasz underscore.js, możesz użyć jego findi indexOffunkcji, aby uzyskać dokładnie to, czego chcesz:

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

Dokumentacja:

Matt Woelk
źródło
Użyj opcji underscorejs tylko wtedy, gdy jest to konieczne, ponieważ załaduj bibliotekę tylko do tego po prostu nie warto
DannyFeliz
4

Począwszy od ES 2015, Array.prototype.find()zapewnia dokładnie taką funkcjonalność.

W przypadku przeglądarek, które nie obsługują tej funkcji, sieć deweloperów Mozilla udostępniła wypełnienie (wklejone poniżej):

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
źródło
2
foundElement = myArray[myArray.findIndex(element => //condition here)];
dotista2008
źródło
3
Przykład z życia z klauzulą ​​warunkową i niektórymi objaśnieniami sprawi, że twoja odpowiedź będzie bardziej wartościowa i zrozumiała.
SaschaM78
0

Mam inspirację z wielu źródeł w Internecie, aby czerpać z poniższego rozwiązania. Chciał wziąć pod uwagę zarówno pewną wartość domyślną, jak i zapewnić sposób na porównanie każdego wpisu dla ogólnego podejścia, które to rozwiązuje.

Zastosowanie: (podając wartość „Second”)

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

Realizacja:

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
źródło
-2

Nie ma wbudowanej funkcji w Javascript do przeprowadzenia tego wyszukiwania.

Jeśli używasz jQuery, możesz zrobić jQuery.inArray(element,array).

PedroSena
źródło
To też by zadziałało, choć pewnie pójdę z Underscore :)
Jakub P.
3
To nie spełnia danych wyjściowych wymaganych przez pytającego (wymaga elementu o pewnym indeksie, a nie logicznej).
Graham Robertson
@GrahamRobertson $.inArraynie zwraca wartości logicznej, (co zaskakujące!) Zwraca indeks pierwszego pasującego elementu. Jednak nadal nie robi tego, o co poprosił PO.
Mark Amery
-2

Jest to mniej elegancki sposób, w którym throwwszystkie prawidłowe komunikaty o błędach (oparte na Array.prototype.filter), ale przestaną iterować od pierwszego wyniku

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

Zatem przykłady są

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

Działa poprzez zakończenie filterza pomocą throw.

Paul S.
źródło