Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Iteracja po tablicach (forEach, map, filter, reduce)

W Parku Jurajskim dane o dinozaurach przyrastają szybciej niż same dinozaury - gatunek, wymiary, dieta, poziom agresji, terminy badań weterynaryjnych. Kiedy to wszystko wyląduje w jednej tablicy, pojedynczy wpis przestaje nas obchodzić. Chcemy odwiedzić każdy element, przerobić go na coś innego, wyłowić tylko te okazy, które wymagają reakcji, albo sprowadzić całe stado do jednej liczby na tablicy w centrum kontroli. JavaScript ma na to osobną rodzinę metod iteracyjnych i każda z nich ma inną specjalność, @name. Trafny wybór to różnica między czytelnym raportem bezpieczeństwa a kodem, do którego nikt nie chce wracać.

Metoda forEach

Metoda

forEach
uruchamia funkcję raz dla każdego elementu tablicy. To najprostszy sposób iteracji - sięgamy po niego wtedy, gdy chcemy z elementami coś zrobić: wypisać je, zapisać do dziennika, wysłać do stacji monitoringu. Nowa tablica nie jest nam wtedy do niczego potrzebna. Funkcja, którą przekazujemy, dostaje bieżący element, a gdy zajdzie taka potrzeba - również jego indeks i całą tablicę.

1// Tablica zawierajaca obiekty reprezentujace dinozaury w naszym parku
2const dinosaurs = [
3  { name: "Tyrannosaurus", diet: "carnivore", aggressionLevel: 10, dangerous: true },
4  { name: "Triceratops", diet: "herbivore", aggressionLevel: 4, dangerous: false },
5  { name: "Velociraptor", diet: "carnivore", aggressionLevel: 9, dangerous: true },
6  { name: "Stegosaurus", diet: "herbivore", aggressionLevel: 2, dangerous: false },
7  { name: "Brachiosaurus", diet: "herbivore", aggressionLevel: 1, dangerous: false }
8];
9
10// Uzywamy forEach do wyswietlenia nazw wszystkich dinozaurow
11console.log("Lista dinozaurow w parku:");
12dinosaurs.forEach(function(dino) {
13  console.log(dino.name);
14});
15
16// Mozemy rowniez uzyc funkcji strzalkowej (arrow function) dla bardziej zwiezlego zapisu
17console.log("Lista dinozaurow z poziomem agresji:");
18dinosaurs.forEach(dino => {
19  console.log(`${dino.name}: Poziom agresji ${dino.aggressionLevel}/10`);
20});

Metoda

forEach
nie oddaje nam niczego - a dokładniej zwraca
undefined
- dlatego używamy jej tylko wtedy, gdy zależy nam na samym działaniu na elementach, a nie na wyniku do dalszego przetworzenia. Warto od razu wiedzieć, czego
forEach
nie potrafi: nie da się jej zatrzymać w połowie.
break
w środku funkcji zwrotnej jest niedozwolone, a
return
kończy wyłącznie bieżące wywołanie i przechodzimy prosto do kolejnego dinozaura. Gdy naprawdę potrzebujemy przerwać przebieg wcześniej, robotę biorą na siebie
find
,
some
,
every
albo klasyczna pętla
for
. Przyjrzyj się jeszcze polu
dangerous
w naszych kartotekach - to flaga, którą regulamin parku podnosi drapieżnikom pod wzmożonym nadzorem. Wrócimy do niej pod koniec lekcji.

Metoda map

Metoda

map
tworzy nową tablicę z wynikami wywołania przekazanej funkcji dla każdego elementu tablicy wejściowej. Jest niezwykle przydatna wszędzie tam, gdzie przekształcamy dane z jednej postaci na drugą - z pełnych kartotek na zwykłą listę nazw albo na krótkie podsumowania bezpieczeństwa, które strażnicy faktycznie zdążą przeczytać przed obchodem.

1// Uzywamy map do stworzenia nowej tablicy zawierajacej tylko nazwy dinozaurow
2const dinoNames = dinosaurs.map(dino => dino.name);
3console.log("Nazwy dinozaurow:", dinoNames);
4// Wynik: ["Tyrannosaurus", "Triceratops", "Velociraptor", "Stegosaurus", "Brachiosaurus"]
5
6// Mozemy tez tworzyc bardziej zlozone transformacje
7const dinoSummaries = dinosaurs.map(dino => {
8  return {
9    name: dino.name,
10    dangerLevel: dino.diet === "carnivore" ? "Wysoki" : "Niski",
11    warning: dino.aggressionLevel > 7 ? "Zachowac szczegolna ostroznosc!" : "Standardowe procedury bezpieczenstwa"
12  };
13});
14
15console.log("Podsumowanie bezpieczenstwa dinozaurow:", dinoSummaries);

W przeciwieństwie do

forEach
, metoda
map
zawsze oddaje nową tablicę o tej samej długości co oryginalna, wypełnioną przekształconymi elementami. I na tym właśnie polega kluczowa różnica między nimi:
map()
zwraca nową tablicę, a
forEach()
zwraca
undefined
. To nie jest kwestia wydajności -
map()
nie jest szybsze, obie metody odwiedzają dokładnie tyle samo elementów, każdy po jednym razie. To także nie jest kwestia przerywania: ani
forEach()
, ani
map()
nie da się zatrzymać w trakcie, więc twierdzenie, że tylko jedną z nich można zatrzymać, jest po prostu nieprawdziwe. Nie działają też identycznie - wywołanie
forEach
w nadziei na odzyskanie tablicy zostawi Cię z
undefined
w ręku. Po
map
sięgamy, gdy potrzebujemy wyniku, po
forEach
- gdy liczy się wyłącznie efekt uboczny.

Metoda filter

Metoda

filter
tworzy nową tablicę zawierającą tylko te elementy, które spełniają zadany warunek. To idealne narzędzie, gdy z większej kolekcji trzeba wyodrębnić podzbiór: mięsożercy spośród wszystkich dinozaurów, wybiegi do inspekcji spośród wszystkich wybiegów, kartoteki do przekazania weterynarzowi spośród wszystkich kartotek.

1// Filtrujemy, aby uzyskac tylko dinozaury miesozerne
2const carnivores = dinosaurs.filter(dino => dino.diet === "carnivore");
3console.log("Miesozerne dinozaury:", carnivores);
4
5// Mozemy laczyc warunki, aby tworzyc bardziej zlozone filtry
6const dangerousDinos = dinosaurs.filter(dino => {
7  return dino.diet === "carnivore" && dino.aggressionLevel > 7;
8});
9console.log("Najbardziej niebezpieczne dinozaury:", dangerousDinos);
10
11// Mozemy tez laczyc filter z map, aby najpierw odfiltrowac, a potem przeksztalcic dane
12const safetyWarnings = dinosaurs
13  .filter(dino => dino.aggressionLevel > 5)
14  .map(dino => `UWAGA: ${dino.name} ma wysoki poziom agresji!`);
15
16console.log("Ostrzezenia bezpieczenstwa:", safetyWarnings);

Metoda

filter
zwraca nową tablicę, która bywa krótsza od oryginalnej, bo trafiają do niej wyłącznie elementy, które przeszły przez "filtr" - czyli te, dla których funkcja testująca zwróciła
true
. Sama tablica
dinosaurs
wychodzi z tego bez zadraśnięcia: podobnie jak
map
,
filter
nigdy nie modyfikuje kolekcji, na której ją wywołujemy, a jedynie o niej raportuje. Zapamiętaj też układ ostatniego przykładu, bo za chwilę okaże się kluczowy: najpierw filtrowanie, dopiero potem transformacja. Ta kolejność nie jest kwestią gustu i zaraz zobaczysz dlaczego.

Metoda reduce

Metoda

reduce
jest najpotężniejsza z metod iteracyjnych, ale też najbardziej wymagająca. Służy do redukcji tablicy do pojedynczej wartości, na przykład sumy. Wykonuje funkcję na każdym elemencie, przekazując dalej wynik z poprzedniej iteracji: w każdym kroku dostajesz to, co udało się nazbierać do tej pory, oraz bieżący element, a oddajesz nową wartość akumulatora.

1// Obliczamy sume poziomow agresji wszystkich dinozaurow
2const totalAggressionLevel = dinosaurs.reduce((total, dino) => {
3  return total + dino.aggressionLevel;
4}, 0); // 0 to wartosc poczatkowa
5
6console.log("Calkowity poziom agresji w parku:", totalAggressionLevel);
7
8// Mozemy uzyc reduce do tworzenia bardziej zlozonych struktur, np. obiektu grupujacego dinozaury wedlug diety
9const dinosByDiet = dinosaurs.reduce((groups, dino) => {
10  // Jesli grupa dla tej diety jeszcze nie istnieje, tworzymy ja
11  if (!groups[dino.diet]) {
12    groups[dino.diet] = [];
13  }
14
15  // Dodajemy dinozaura do odpowiedniej grupy
16  groups[dino.diet].push(dino.name);
17
18  return groups;
19}, {}); // {} to wartosc poczatkowa (pusty obiekt)
20
21console.log("Dinozaury pogrupowane wedlug diety:", dinosByDiet);
22// Wynik: { "carnivore": ["Tyrannosaurus", "Velociraptor"], "herbivore": ["Triceratops", "Stegosaurus", "Brachiosaurus"] }

Metoda

reduce
przyjmuje dwa parametry:

  1. Funkcję redukcji, która sama przyjmuje co najmniej dwa parametry:
    • Akumulator: wartość zwrócona z poprzedniej iteracji (lub wartość początkowa)
    • Bieżący element: aktualnie przetwarzany element tablicy
  2. Wartość początkową (opcjonalną, ale zdecydowanie zalecaną)

Nazwa tej metody bywa myląca, więc uporządkujmy, do czego

reduce()
nie służy. Nie usuwa elementów z tablicy - obie powyższe operacje przeczytały nasze kartoteki i zostawiły je w spokoju. Nie zmniejsza też rozmiaru tablicy: po wykonaniu obu wywołań
dinosaurs
nadal liczy pięć wpisów, co do jednego. Nie sortuje niczego malejąco - porządkowaniem elementów zajmuje się
sort()
, którą poznasz za moment. Słowo "redukcja" oznacza tu dokładnie jedno: sprowadzenie całej tablicy do pojedynczej wartości, czy będzie to liczba, tekst, czy obiekt grupujący pokroju
dinosByDiet
.

Metoda find

Metoda

find
zwraca pierwszy element tablicy, który spełnia podany warunek. Po nią sięgamy wtedy, gdy szukamy konkretnej kartoteki, a nie całej listy: strażnik, który prosi "otwórz mi teczkę Stegozaura", nie oczekuje tablicy, tylko jednego dinozaura.

1// Szukamy dinozaura o okreslonej nazwie
2const stegosaurus = dinosaurs.find(dino => dino.name === "Stegosaurus");
3console.log("Informacje o Stegosaurusie:", stegosaurus);
4
5// Szukamy pierwszego dinozaura miesozernego o agresji wiekszej niz 8
6const dangerousCarnivore = dinosaurs.find(dino => {
7  return dino.diet === "carnivore" && dino.aggressionLevel > 8;
8});
9console.log("Pierwszy niebezpieczny miesozerca:", dangerousCarnivore); // Tyrannosaurus
10
11// findIndex dziala tak samo, ale zwraca pozycje zamiast kartoteki
12const stegoIndex = dinosaurs.findIndex(dino => dino.name === "Stegosaurus");
13console.log("Pozycja Stegosaurusa:", stegoIndex); // 3

W przeciwieństwie do

filter
, który zwraca tablicę wszystkich pasujących elementów,
find
oddaje tylko pierwsze trafienie i natychmiast przerywa przeszukiwanie - pozostałe dinozaury nie zostaną nawet obejrzane. Jeśli nic nie pasuje, dostajemy
undefined
. Jej bliźniacza siostra
findIndex
kieruje się tą samą logiką, ale zamiast elementu podaje jego indeks, a przy braku dopasowania zwraca
-1
. Bywa to nieocenione, gdy zaraz potem trzeba podmienić kartotekę leżącą na tej pozycji.

Metody some i every

Metody

some
i
every
zwracają wartość boolowską i służą do sprawdzania, czy elementy tablicy spełniają określone kryteria. Żadna z nich nie buduje nowej tablicy ani nie oddaje elementów - odpowiadają po prostu
true
albo
false
na pytanie o całą kolekcję, co jest dokładnie tym, czego potrzebuje alarm albo kontrola bezpieczeństwa przed otwarciem parku.

some

Metoda

some
sprawdza, czy przynajmniej jeden element tablicy spełnia podany warunek. Gdy tylko natrafi na pasujący element, kończy przebieg i zwraca
true
- dalsze szukanie niczego by już nie zmieniło.

1// Sprawdzamy, czy w parku sa jakies niebezpieczne dinozaury
2const hasHighAggressionDinos = dinosaurs.some(dino => dino.aggressionLevel > 8);
3console.log("Czy park ma dinozaury o wysokiej agresji?", hasHighAggressionDinos); // true
4
5// Sprawdzamy, czy w parku sa jakies dinozaury wszystkozerne
6const hasOmnivores = dinosaurs.some(dino => dino.diet === "omnivore");
7console.log("Czy park ma dinozaury wszystkozerne?", hasOmnivores); // false

Pierwsze wywołanie zwraca

true
, bo Tyranozaur przekracza próg już przy pierwszym sprawdzanym elemencie i dalej nie ma po co iść. Drugie musi przejść całą listę, zanim wyda werdykt: żadna kartoteka nie ma diety
"omnivore"
, więc odpowiedź brzmi
false
. To właśnie ta rodzina metod potrafi przerwać przebieg w połowie - o
forEach
i
map
pamiętaj, że zawsze idą do samego końca tablicy.

every

Metoda

every
sprawdza, czy wszystkie elementy tablicy spełniają podany warunek. Działa symetrycznie do
some
: zatrzymuje się na pierwszym elemencie, który obleje test, i od razu zwraca
false
.

1// Sprawdzamy, czy wszystkie dinozaury maja przypisany poziom agresji
2const allHaveAggressionLevel = dinosaurs.every(dino => dino.aggressionLevel !== undefined);
3console.log("Czy wszystkie dinozaury maja przypisany poziom agresji?", allHaveAggressionLevel); // true
4
5// Sprawdzamy, czy wszystkie dinozaury sa roslinozerne
6const allHerbivores = dinosaurs.every(dino => dino.diet === "herbivore");
7console.log("Czy wszystkie dinozaury sa roslinozerne?", allHerbivores); // false

Pierwsza kontrola wypada pomyślnie: każda z pięciu kartotek ma uzupełnione pole

aggressionLevel
, więc
every
przechodzi całą tablicę i zwraca
true
. Druga wykłada się już na pierwszym wpisie - Tyranozaur jest mięsożercą - i zwraca
false
, nie zaglądając do pozostałych. Najłatwiej zapamiętać tę parę jako dwa pytania zadawane na głos:
some
pyta "czy jest przynajmniej jeden?", a
every
pyta "czy wszystkie?".

Metoda sort

Metoda

sort
porządkuje elementy tablicy i zwraca posortowaną tablicę. Domyślnie porównuje je jak ciągi znaków, co przy liczbach potrafi zaskoczyć - tablica
[10, 9, 100]
wychodzi z takiego sortowania jako
[10, 100, 9]
, bo znak po znaku "1" wypada przed "9". Właśnie dlatego niemal zawsze przekazujemy jej własną funkcję porównującą.

1// Sortujemy dinozaury wedlug nazwy (alfabetycznie)
2const dinosSortedByName = [...dinosaurs].sort((a, b) => {
3  return a.name.localeCompare(b.name);
4});
5console.log("Dinozaury posortowane alfabetycznie:", dinosSortedByName);
6
7// Sortujemy dinozaury wedlug poziomu agresji (od najmniej do najbardziej agresywnych)
8const dinosSortedByAggression = [...dinosaurs].sort((a, b) => {
9  return a.aggressionLevel - b.aggressionLevel;
10});
11console.log("Dinozaury posortowane wedlug agresji (rosnaco):", dinosSortedByAggression);
12
13// Sortujemy dinozaury wedlug poziomu agresji (od najbardziej do najmniej agresywnych)
14const mostDangerousFirst = [...dinosaurs].sort((a, b) => {
15  return b.aggressionLevel - a.aggressionLevel;
16});
17console.log("Dinozaury posortowane wedlug agresji (malejaco):", mostDangerousFirst);

Kopię tablicy tworzymy operatorem spread (

...
) nie bez powodu:
sort
modyfikuje oryginał, a my chcemy zachować pierwotną kolejność rejestru parku. To wielki wyjątek wśród poznanych dotąd metod -
map
,
filter
i
reduce
zostawiają źródło nietknięte,
sort
nie. O kierunku decyduje funkcja porównująca: wynik ujemny stawia
a
przed
b
, dlatego
a.aggressionLevel - b.aggressionLevel
układa okazy rosnąco, a zamiana operandów miejscami daje porządek malejący. I to
sort
, wyłącznie
sort
, odpowiada w JavaScripcie za sortowanie rosnące i malejące.

Łączenie metod

Jedną z najpotężniejszych cech tych metod jest możliwość łączenia ich w "łańcuchy", dzięki czemu złożoną operację na danych zapisujemy jako jedno czytelne wyrażenie. Działa to dlatego, że

filter
,
map
i
sort
oddają tablicę, więc kolejną metodę wywołujemy wprost na wyniku poprzedniej. Najczęstszy wzorzec w parku składa się z dwóch kroków: filtrowania, a zaraz po nim transformacji.

1// Zrodlo, potem filtrowanie, potem transformacja
2const dangerousNames = dinosaurs
3  .filter(d => d.dangerous)   // 1. zostawiamy tylko kartoteki z flaga dangerous
4  .map(d => d.name);          // 2. z kazdej ocalalej kartoteki bierzemy sama nazwe
5
6console.log("Nazwy niebezpiecznych dinozaurow:", dangerousNames);
7// Wynik: ["Tyrannosaurus", "Velociraptor"]

Ten łańcuch czytamy od lewej do prawej, a kolejność jego ogniw jest zawsze taka sama: najpierw źródło danych

dinosaurs
, potem filtrowanie
.filter(d => d.dangerous)
, a na końcu transformacja
.map(d => d.name)
. Ta kolejność nie podlega negocjacjom, @name. Gdyby zamienić dwa ostatnie ogniwa miejscami,
map
oddałoby tablicę zwykłych napisów, a
filter
szukałoby potem właściwości
dangerous
na tekście - wszędzie wypadłoby
undefined
i w wyniku dostalibyśmy pustą tablicę. Filtrujemy, dopóki mamy jeszcze pełne obiekty, przekształcamy dopiero potem. Gdy ten odruch wejdzie Ci w krew, łańcuch możesz wydłużać dowolnie i wplatać w niego
sort
albo
reduce
.

1// Znajdzmy nazwy wszystkich dinozaurow roslinozernych, posortowane alfabetycznie
2const sortedHerbivoreNames = dinosaurs
3  .filter(dino => dino.diet === "herbivore")  // Najpierw filtrujemy
4  .map(dino => dino.name)                     // Potem transformujemy do tablicy nazw
5  .sort();                                    // Na koncu sortujemy alfabetycznie
6
7console.log("Roslinozercy (alfabetycznie):", sortedHerbivoreNames);
8// Wynik: ["Brachiosaurus", "Stegosaurus", "Triceratops"]
9
10// Obliczmy sredni poziom agresji miesozercow
11const averageCarnivoreAggression = dinosaurs
12  .filter(dino => dino.diet === "carnivore")   // Najpierw filtrujemy tylko miesozercow
13  .reduce((sum, dino, index, array) => {       // Potem redukujemy do sredniej
14    sum += dino.aggressionLevel;               // Dodajemy poziom agresji do sumy
15
16    // Jesli to ostatni element, dzielimy przez liczbe elementow
17    if (index === array.length - 1) {
18      return sum / array.length;
19    }
20
21    return sum;                               // Zwracamy sume czesciowa
22  }, 0);
23
24console.log("Sredni poziom agresji miesozercow:", averageCarnivoreAggression); // 9.5

Drugi przykład pokazuje, jak elastyczna potrafi być

reduce
. Funkcja redukcji dostaje nie tylko akumulator i bieżący element, ale też indeks oraz całą przetwarzaną tablicę, dzięki czemu potrafimy rozpoznać ostatnią iterację i w odpowiednim momencie podzielić zebraną sumę. Mięsożerców mamy dwóch, ich poziomy agresji to 10 i 9, więc na wyjściu dostajemy 9.5 - jedną liczbę wyciśniętą z całej tablicy kartotek, czyli dokładnie to, do czego
reduce
została stworzona.

Podsumowanie

Metody iteracyjne tablic w JavaScript są nieocenionym narzędziem przy pracy z kolekcjami danych. Każda ma swoją specjalizację:

  • forEach: gdy chcemy wykonać jakąś operację na każdym elemencie, bez tworzenia nowej tablicy (zwraca
    undefined
    )
  • map: gdy chcemy przekształcić każdy element tablicy w coś innego (zwraca nową tablicę)
  • filter: gdy chcemy wybrać podgrupę elementów spełniających określone kryteria
  • reduce: gdy chcemy zredukować tablicę do pojedynczej wartości, na przykład sumy, albo do nowej struktury
  • find: gdy szukamy pierwszego elementu spełniającego określone kryterium
  • some/every: gdy chcemy sprawdzić, czy pewne/wszystkie elementy spełniają określone kryterium
  • sort: gdy chcemy uporządkować elementy tablicy

"Metody iteracyjne to poranna odprawa w parku" - mawia Dr. Rex. "

forEach
przechodzi codzienną listę kontrolną,
filter
wyławia tylko te okazy, które wymagają uwagi,
map
przerabia je na raport, a
reduce
daje centrum kontroli jedną liczbę, na której da się oprzeć decyzję. Każde narzędzie ma swoje zadanie!"

W naszym Parku Jurajskim te metody są niezbędne do efektywnego zarządzania danymi o dinozaurach, monitorowania bezpieczeństwa i podejmowania decyzji opartych na danych. Zapamiętaj przede wszystkim jedno: metodę dobieramy według tego, co chcemy dostać - efekt uboczny, nową tablicę, podzbiór czy pojedynczą wartość - a reszta kodu napisze się już niemal sama.

Przejdź do CodeWorlds