Przeszukiwanie tablic w JavaScript – przegląd wybranych metod

Dziś omówimy kilka wybranych metod umożliwiających przeszukiwanie tablic w JavaScript. Teoretycznie jest to proste zadanie, ale jak się okaże obecnie dysponujemy różnymi metodami Array.prototype w zależności od tego, w jakim celu i jakich danych poszukujemy.

W artykule omówię część metod operujących na tablicach, w tym:

  • Array.prototype.indexOf()
  • Array.prototype.lastIndexOf()
  • Array.prototype.includes()
  • Array.prototype.find()
  • Array.prototype.findIndex()

Przeszukiwanie tablicy można również przeprowadzić przy użyciu tradycyjnych pętli, lecz to zagadnienie omawiałem już we wcześniejszym wpisie: Iterowanie po tablicach.

W artykule nie będziemy zagłębiać się w tematykę szybkości działania metod Array.prototype vs pętle for, gdyż w większości typowych przypadków nie będzie miało to istotnego znaczenia, a przed tego typu dyskusją myślę, że warto dokładnie omówić zarówno pętle jak i metody tablicowe.

Zacznijmy od popularnej metody indexOf()

 

Najczęściej stosowaną metodą do przeszukiwania tablic (oraz ciągów znakowych) jest metoda indexOf(). Obecnie dysponujemy jednak kilkoma innymi metodami z zaplecza Array.prototype, które dają więcej możliwości kontrolowania sposobu szukania elementów w tablicy.

Metoda indexOf() wśród początkujących programistów rodzi pewne problemy, gdyż pobiera ona jako argument wartość do znalezienia, ale zwrotnie otrzymujemy nie wartość boolowską true/false lecz numer indeksu znalezionego elementu.

Należy pamiętać również o tym, że metoda indexOf() w przypadku braku znalezienia szukanej wartości zwraca wartość: -1.

let arr = [1,2,3,'a','b',null,'1','2','3'];
arr.indexOf(1);    //0
arr.indexOf('1');  //6
arr.indexOf(null); //5
arr.indexOf(15);   //-1

A co gdy chcemy zwrotnie otrzymać informację tylko o tym, czy dana wartość znajduje się w tablicy, czyli wartość true/false, np. w celu użycia jej w instrukcji warunkowej IF?

if (arr.indexOf(2) !== -1) {
    //kod, gdy znaleziono wartość: 2
}

if (~arr.indexOf(2)) {
    //kod gdy znaleziono wartość: 2
}

W pierwszym przypadku wynik zwracany metodą indexOf() przyrównujemy do: -1. Nie możemy zastosować bezpośrednio zapisu:  if(arr.indexOf(1)) gdyż narażamy się na błąd logiczny w przypadku, gdy szukana wartość (u nas: 1) jest pierwszym elementem tablicy. Metoda zwróci wtedy wartość 0 (zero), co zostanie niejawnie przekonwertowane na false.

W drugim przykładzie zastosowałem krótszą formę z użyciem bitowego operatora negacji (tylda: ~). Rozwiązanie to omawiałem w artykule: Przeszukiwanie ciągów znakowych. W przypadku braku dopasowania, czyli zwrócenia wartości -1, operator bitowy NOT zwróci wartość zero, czyli niejawne false. Dla każdego innego indeksu zostanie zwrócona wartość rożna od zera, która zostanie przekonwertowana na true.

Metoda lastIndexOf()

Obiekty dziedziczące po Array.prototype posiadają również metodą lastIndexOf(), która przeszukuje tablicę w sposób podobny jak indexOf() lecz robi to od końca (czyli od prawej do lewej).

let array = [1,2,3,3,2,1];

array.indexOf(2);     //1
array.lastIndexOf(2); //4

Widać tutaj więc, że metoda lastIndexOf() zaczęła przeszukiwanie tablicy od końca. Należy jednak zwrócić uwagę, że nadal zwracany jest rzeczywisty numer indeksu znalezionej wartości (czyli liczony od zera, od lewej do prawej).

Wyszukiwanie od ustalonego indeksu

Zarówno metoda indexOf() jak również lastIndexOf() mogą pobierać drugi, opcjonalny argument. Określa on miejsce, od którego następuje rozpoczęcie przeszukiwania tablicy.

Zalecam jednak dużą ostrożność w stosowaniu drugiego argumentu, szczególnie w metodzie lastIndexOf() jeśli nie znamy dokładnie zasady jego działania. Spójrzmy na poniższe przykłady. Celowo w tabeli umieściłem wartości typu znakowego, aby wyraźnie odróżnić wartość elementu i jego indeks (numeryczny).

let arr = ['a', 'b', 'c', 'b', 'a'];

arr.indexOf('b');   //1
arr.indexOf('b',1); //1 (ignorujemy indeks: 0)
arr.indexOf('b',2); //3 (ignorujemy indeksy: 0,1)

//I teraz zaczynają się schody... :)
arr.lastIndexOf('b');   //3
arr.lastIndexOf('b',3); //3 (ignorujemy indeks: 4)
arr.lastIndexOf('b',2); //1 (ignorujemy indeksy: 3,4)
arr.lastIndexOf('c',2); //2 (j.w.)

W przypadku metody indexOf() wyszukiwanie zaczynamy po prostu od wskazanego indeksu (drugi argument), przy czym indeks ten uczestniczy w przeszukiwaniu (patrz: drugie wywołanie indexOf()). Tutaj w zasadzie nie ma nic więcej do wyjaśniania. Można więc w uproszczeniu powiedzieć, że metoda jakby „nie widzi” elementów o indeksach od zera do N (gdzie N to właśnie nasz drugi parametr), ale zwróć uwagę, że metoda zwraca zawsze faktyczny indeks danego elementu (a więc odnosi się zawsze do całej tablicy).

Częste wątpliwości budzi natomiast zachowanie się metody lastIndexOf() w połączeniu z jej drugim argumentem. Sprawa jest jednak bardzo prosta, otóż drugi argument powoduje „odcięcie” elementów tablicy o indeksach większych niż N. W powyższych przykładach mamy wywołanie arr.lastIndexOf('b',2) co powoduje zignorowanie w przeszukiwaniu elementów o indeksach 3 i 4, więc de facto przeszukujemy tablicę ['a','b','c']. Ale uwaga… metoda lastIndexOf() nadal szuka wartości od prawej do lewej, lecz teraz skrajnie prawym elementem będzie wartość ‚c’.

Ogólnie mówiąc, można w dużym uproszczeniu przyjąć, że drugi argument w metodzie indexOf() określa do którego indeksu mamy „zignorować” elementy tablicy (czyli ignorowanie od lewej). W metodzie lastIndexOf() argument ten określa z kolei od którego elementu mamy ignorować elementy tablicy ale licząc od N w prawo. Jest to jednak mocno kolokwialne stwierdzenie, gdyż w praktyce tablica w żaden sposób nie jest ucinana (ani de facto ignorowana).

A co jeśli drugim argumentem będzie liczba ujemna?

No to jak już omówiliśmy zastosowanie drugiego argumentu w celu „ucinania” części tablicy zastanówmy się, czy może on być również liczbą ujemną? Otóż tak, lecz indeks liczony jest wtedy od drugiej strony tablicy. Aby lepiej to zrozumieć prześledźmy poniższe przykłady:

let arr = ['a', 'b', 'c', 'b', 'a'];

arr.indexOf('b',1);  //1 (ignorujemy indeks: 0)
arr.indexOf('b',-1); //-1 (ignorujemy indeksy: 0,1,2,3)
arr.indexOf('a',1);  //4 (ignorujemy indeks: 0)

arr.lastIndexOf('b',2);  //1 (ignorujemy indeksy: 3,4)
arr.lastIndexOf('b',-2); //3 (ignorujemy indeks: 4)

arr.lastIndexOf('a',-1); //4 (nie ignorujemy żadnych indeksów)
arr.lastIndexOf('a');    //4

Zwróćmy tutaj uwagę na sposób numerowania indeksów. Jeśli drugi argument jest liczbą dodatnią to pierwszy lewy element jest traktowany jako element zerowy, dlatego wywołanie arr.indexOf('a',1) zignorowało element o indeksie zerowym, czyli arr[0]==='a'. Jeśli jednak argument ten podajemy jako liczbę ujemną, to wartość -1 odnosi się do pierwszego elementu z prawej strony (czyli ostatniego elementu tablicy).

Sposób porównywania wartości w metodach indexOf() i lastIndexOf()

Tablice w języku JavaScript mogą przechowywać wartości różnego typu. Oznacza to, że nasza tablica testowa jest w pełni prawidłowa, mimo, że zawiera aż 3 typy wartości (number, string, null).

Trzeba jednak pamiętać, że metody Array.prototype.indexOf oraz Array.prototype.lastIndexOf zawsze porównują elementy tablicy z wartością szukaną przy użycia operatora identyczności (===). Musi się więc zgadzać zarówno sama wartość jak i jej typ.

1 == '1';  //true
1 === '1'; //false
let arr = [1,2,3,'4','5'];

arr.indexOf(2);   //1
arr.indexOf(5);   //-1 nie znaleziono wartości: 5
arr.indexOf('5'); //4 tym razem znaleziono: '5'

Dokładnie w taki sam sposób działa metoda lastIndexOf(). Trzeba o tym pamiętać np. jeśli wrzucamy do tablicy wartości elementów wyciąganych z DOM (wartości pól formularzy zawsze pobieramy jako string) i później chcemy wyszukiwać wartości liczbowe.

„Ulepszone” testowanie obecności wartości w tablicy

Problem z metodą indexOf() jest taki, że zwraca ona zawsze albo indeks znalezionej wartości albo wartość: -1. W praktyce najczęściej potrzebujemy jednak innej informacji, a mianowicie: czy wartość ‚x’ znajduje się w tablicy?

Oczekujemy więc zwrotnie wartości typu boolean: true/false. Jedną z metod jest stosowanie przyrównywania wyniku indexOf() do -1 lub używanie operatora „~”. Lepszym rozwiązaniem jest jednak wykorzystanie metody Array.prototype.includes().

let arr = [1,2,3,'a','b',null,'4','5'];

arr.includes(2);    //true
arr.includes(null); //true
arr.includes(4);    //false
arr.includes('4');  //true

Jest to znacznie krótsza i czytelniejsza forma, a ponad to od razu otrzymujemy zwrotną wartość true/false.

Należy zauważyć, że metoda includes(), podobnie jak omawiane wcześniej indexOf() oraz lastIndexOf() stosują porównanie ścisłe (===), dlatego wyszukanie wartości: 4 typu numerycznego kończy się niepowodzeniem (false), a wyszukanie ciągu typu string o wartości ‚4’ daje true.

Metoda includes() może pobierać również drugi, opcjonalny argument, który określa indeks od którego ma nastąpić rozpoczęcie przeszukiwania tablicy. Odbywa się to na takich samych zasadach jak przy metodzie indexOf(), którą omawialiśmy wcześniej.

let arr = ['a','b','a','a','c'];

arr.includes('b');    //true
arr.includes('b',1);  //true
arr.includes('b',2);  //false

arr.includes('b',-3); //false
arr.includes('b',-4); //true

Jeśli któreś z wywołań metody includes z drugim parametrem jest dla Ciebie niezrozumiałe to proponuję cofnąć się do początku artykułu, gdzie omawiałem dokładniej to zagadnienie w metodach indexOf()lastIndexOf().

Metoda includes(), choć daje duże możliwości szybkiej analizy tablicy to stanowi element dodany dopiero w standardzie ECMAScript 6 i nie jest ona obsługiwana m.in. w przeglądarkach Internet Explorer. Aby swobodnie z niej korzystać powinniśmy więc wgrać odpowiedni polyfill dodający metodę do łańcucha prototypów Array.prototype.

W niektórych poradnikach można znaleźć informację, że pewną alternatywą dla includes() jest metoda some(), lecz osobiście nie zgadzam się z takim twierdzeniem. Metody te służą do różnych celów i pobierają różne argumenty (metoda some() pobiera funkcję zwrotną callback i opcjonalny obiekt wskazujący na this). Z tego względu nie będę dzisiaj omawiał metody some(), którą zajmiemy się kiedy indziej.

Bardziej zaawansowane wyszukiwanie metodą find()

Zastosowanie metody indexOf() oraz lastIndexOf() jest najczęściej spotykanym sposobem przeszukiwania wartości tablic. W standardzie ECMAScript 6 dostaliśmy nową metodę Array.prototype.find(), której zadaniem jest wyszukanie elementu spełniającego określone warunki, przy czym mamy tutaj praktycznie dowolność w zakresie formułowania warunku wyszukiwania.

Metoda find() pobiera bowiem jako pierwszy argument nie wartość szukanego elementu lecz funkcję zwrotną. Funkcja ta z kolei pobiera trzy argumenty: wartość iterowanego aktualnie elementu, jego indeks oraz tablicę wejściową. Metoda find zwraca pierwszy element tablicy, dla którego funkcja callback zwróciła wartość true. Widzimy zatem, że dysponujemy potężnym narzędziem do praktycznie dowolnej analizy zawartości tablicy. Teoretycznie może wydawać się, że metoda find() działa podobnie do metody some(), lecz istnieje między nimi jedna zasadnicza różnica: metoda find() nie zwraca wartości true/false lecz wartość pierwszego elementu uznanego za „prawdziwy” w funkcji callback.

Załóżmy, że mamy za zadanie sprawdzić, czy tablica zawiera liczby nieparzyste i jeśli tak, to chcemy zwrotnie otrzymać wartość pierwszej napotkanej liczby nieparzystej (wiem wiem, zadanie nieco durnowate, ale traktujemy to jako ćwiczenie :-):

let arr = [2,4,6,8,10,15,20,17]; //nieparzyste 15 i 17

arr.find((element) => element&1); //15
arr.find((element) => element > 100); //undefined

Każde kolejne wywołanie metody find() w tej postaci również zwróci wartość 15 gdyż metoda będzie operować na tej samej tablicy. Jeśli chcemy otrzymać zwrotnie wszystkie elementy tablicy spełniające kryteria funkcji zwrotnej to możemy zastosować jedną z pętli for (np. for-of) z zapisaniem wyników w dodatkowej tablicy result. Alternatywnie można zastosować wielokrotne wywołania metody find() z przekazaniem jej funkcji generatora, która mogłaby mieć możliwość zapamiętania ostatniej pozycji w tablicy. Sposób ten wykracza jednak poza ramy dzisiejszego tematu (choć myślę, że powrócimy do niego niedługo gdy będę omawiał generatory ES6).

Należy również zauważyć, że metoda find() zwraca undefined w przypadku braku dopasowania jakiegokolwiek z elementów do kryteriów funkcji callback. Przypomnijmy, że metoda indexOf() zwraca w przypadku niepowodzenia wartość: -1, natomiast metoda some() zwróci zawsze wartość boolean true/false.

Wyszukiwanie indeksu elementu wg metody findIndex()

Metoda findIndex() działa bardzo podobnie do metody find() z tą różnicą, że zwraca nie wartość pierwszego dopasowanego elementu lecz jego indeks, czyli w pewnym sensie jest bliższa metodzie indexOf().

Przyjmuje ona również jako pierwszy argument funkcję zwrotną callback, która powinna zwracać jawnie true/false. Zarówno w metodzie findIndex() jak i metodzie find() funkcja callback nie musi w razie niepowodzenia jawnie przekazywać wartości false, gdyż przy dobrze skonstruowanej funkcji powinna ona zwrócić wtedy niejawnie undefined, co zostanie automatycznie przekonwertowane na wartość false.

Aby opis metody findIndex() był pełny zróbmy jeszcze jakiś przykład 🙂

let arr = [2,4,6,8,10,15,20,17]; //nieparzyste 15 i 17

arr.findIndex((element) => element&1); //5
arr.findIndex((element) => element > 100); //-1

Przyjrzyj się uważnie drugiemu wywołaniu metody findIndex(). Otóż w przypadku niepowodzenia metoda ta nie zwraca undefined (co byłoby teoretycznie oczekiwane na podstawie wartości zwracanych przez metodę find()) lecz zwraca wartość: -1 (podobnie jak indexOf()). Należy o tym pamiętać jeśli będziemy chcieli używać tych metod w instrukcjach warunkowych.

Zarówno metoda find() jak i findIndex() mogą pobierać drugi, opcjonalny argument, który określa obiekt, na który będzie wskazywać this użyte wewnątrz funkcji zwrotnej callback. Tutaj jednak mała uwaga… jawne wskazanie this nie działa jeśli w formie funkcji zwrotnej użyjemy składni arrow function. Więcej na ten temat napisałem we wpisie Funkcje strzałkowe arrow function.

Wyszukanie elementu w tablicy przy użyciu wyrażeń regularnych

Osobiście jestem wielkim fanem tematyki wyrażeń regularnych (regexp) więc nie mogłem sobie odpuścić problemu przeszukiwania tablic z wykorzystaniem RegExp’ów. Pytanie czy jest to w ogóle możliwe w języku JavaScript?

Odpowiedź brzmi: TAK, i to w bardzo prosty sposób. Cofnijmy się nieco wstecz i przypomnijmy sobie o omawianej przeze mnie metodzie  Array.prototype.find. Otóż przyjmuje ona jako pierwszy parametr funkcję zwrotną. Jeśli funkcja ta zwróci dla danego elementu wartość true to element zostanie uznany za „znaleziony” i zwrócony przez metodę find. Skoro przekazujemy funkcję zwrotną to tak na prawdę nic nas nie ogranicza i możemy korzystać również z wyrażeń regularnych:

let arr = ['abCD', '12-06-1986', '12345'];

arr.find((val) => /^\d+$/.test(val));      //'12345'
arr.findIndex((val) => /^\d+$/.test(val)); //2

arr.find((val) => /^(\d\d-){2}\d{4}$/.test(val)); //'12-06-1986'

arr.findIndex((val) => /^[a-z]+$/i.test(val)); //0

//Czy istnieje w tablicy jakiś ciąg literowy?
!!~arr.find((val) => /^[a-z]+$/i.test(val)); //true
arr.some((val) => /^[a-z]+$/i.test(val));    //true

Zapewne nie będzie to często spotykana praktyka, ale uważam, że dobrze wykorzystane wyrażenia regularne są potężnym narzędziem w rękach programisty i pozwalają na bardzo precyzyjne ustalanie kryteriów analizowania ciągów znakowych.

Należy jednak pamiętać, że metody związane z wyrażeniami regularnymi to metody RegExp.prototype albo String.prototype. Na przykład wywołanie /\d+/.test(12345) da taki sam efekt jak /\d+/.test('12345') to osobiście uważam za dobrą praktykę jawne przekazywanie wartości odpowiedniego typu. W powyższym przykładzie nastąpi niejawne wywołanie (12345).toString() ale do wszystkich niejawnych konwersji należy podchodzić z ostrożnością chyba, że robimy to w pełni świadomie i znając zasady konwersji.

Podsumowanie – czyli jaką metodą przeszukiwać tablice?

W artykule omówiliśmy kilka wybranych metod przeszukujących tablice. Należy zauważyć, że różnią się one nie tylko składnią (typami pobieranych argumentów) lecz również zwracanymi wartościami w przypadku pozytywnego dopasowania elementu oraz przy przeszukaniu negatywnym.

Wybór metody zależy więc od konkretnej sytuacji, a przede wszystkim od wybranej metody przeszukiwania tablicy oraz od oczekiwanych wyników zwrotnych.

Jeszcze jedna mała uwaga na koniec. W artykule omówiono metody zarówno ze standardu ES3/5 jak i ES6. Zanim zastosujesz jakąś metodę warto wcześniej sprawdzić czy wymaga ona użycia ewentualnego polyfill lub w jaki sposób jest interpretowana przez transpilator kodu. Odnosi się to głównie do środowiska przeglądarki użytkownika, dlatego zawsze należy jasno sobie określić dla jakich środowisk projektujemy aplikację.

  • Dzięki za artykuł. Jak zwykle konkretny i obszerny.
    Może warto pomyśleć o takim liczniku ile zajmuje przeczytanie artykułu? https://piecioshka.pl/blog/2016/12/16/dlugosc-artykulu-w-minutach.html

    BTW Funkcja Array.prototype.includes została zdefiniowana w ECMAScript 2016 http://kangax.github.io/compat-table/es2016plus/

    Specyfikacja ECMAScript 2017 nie została jeszcze zatwierdzona.

    • Tomasz Sochacki

      Faktycznie, masz rację z includes. Szczerze mówiąc wyczytałem na jakimś z blogów, że jest to ES7 i jakoś przyjąłem to do wiadomości nie wnikając w szczegóły.
      W kwestii licznika czasu to nawet niedawno myślałem o tym właśnie po lekturze Twojego wpisu ale w natłoku zadań temat wyparował z głowy 🙂 Pytanie tylko na ile jest to faktycznie adekwatne w przypadku większej ilości kodu, który wiadomo, że analizuje się wolniej niż tekst, ale postaram się za jakiś czas pomyśleć o tym i zobaczymy w praktyce.

  • Leni

    Napisałeś, że przy indexOf gdy drugi argument podajemy ujemny to jest on liczony od pierwszego po prawej. Lecz nie napisałeś co się dzieje przy lastIndexOf. Też liczy od prawej tylko pierwsza pozycja to -2?

    • W tym wypadku drugi argument również „ucina” tablicę od prawej strony (wartość -1 oznacza pierwszy element od prawej), ale metoda lastIndexOf nadal wyszykuje od prawej do lewej. Zrobiłem na to kilka przykładów w moich kodach, ale w sumie może faktycznie w wolnej chwili nieco dokładniej do opiszę i dam więcej przykładów. Pozdrawiam