Kurs JavaScript i TypeScript · Moduł 12: Programowanie funkcyjne

Funkcje wyższego rzędu: map, filter, reduce

6 min czytania
W tej lekcji5

Poranna odprawa wymaga trzech zestawień naraz: listy nazw wszystkich okazów, wykazu chorych drapieżników i łącznej liczby dinozaurów w parku. Trzy pętle for to trzy liczniki, trzy tymczasowe tablice i trzy okazje do pomyłki o jeden indeks. Da się to zrobić krócej i bez ruszania danych źródłowych.

W laboratoriach Parku Jurajskiego naukowcy codziennie przetwarzają ogromne ilości danych o dinozaurach - filtrują próbki DNA, transformują wyniki analiz i agregują statystyki. Funkcje wyższego rzędu (higher-order functions) to narzędzia, które pozwalają robić to samo z danymi w JavaScript - elegancko, czytelnie i bezpiecznie.

Czym jest funkcja wyższego rzędu?

Funkcja wyższego rzędu to funkcja, która przyjmuje inną funkcję jako argument lub zwraca funkcję jako wynik. To jak maszyna w laboratorium, która przyjmuje instrukcję (funkcję) i stosuje ją do każdej próbki DNA.

Jedną już znasz: metoda filter z poprzedniej lekcji przyjmowała funkcję isCarnivore. Przekazaną instrukcję nazywamy callbackiem - metoda sama wywołuje ją dla kolejnych elementów tablicy, a Ty decydujesz tylko, co ma się stać z jednym elementem.

Array.map() - transformacja danych

map tworzy nową tablicę, stosując podaną funkcję do każdego elementu. Oryginalna tablica pozostaje nienaruszona.

Mamy trzy okazy i potrzebujemy samej listy ich nazw:

1const specimens = [
2  { name: 'Rex', weight: 8000, age: 5 },
3  { name: 'Blue', weight: 150, age: 3 },
4  { name: 'Brachio', weight: 56000, age: 12 },
5];
6
7// Transformacja - wydobycie samych nazw
8const names = specimens.map((dino) => dino.name);
9// ['Rex', 'Blue', 'Brachio']

Wynik ma zawsze tyle elementów, ile tablica wejściowa, bo map niczego nie odrzuca, tylko przekształca. specimens nadal zawiera pełne obiekty.

Callback może też zbudować zupełnie nowy obiekt. Zwróć uwagę na nawiasy okrągłe wokół nawiasów klamrowych: bez nich JavaScript uznałby { za początek ciała funkcji, a nie literał obiektu.

1// Transformacja - obliczenie raportu
2const reports = specimens.map((dino) => ({
3  label: dino.name.toUpperCase(),
4  weightInTons: dino.weight / 1000,
5  category: dino.weight > 5000 ? 'large' : 'small',
6}));
7// [{ label: 'REX', weightInTons: 8, category: 'large' }, ...]

Każdy raport to nowy obiekt: toUpperCase zamienia nazwę na wielkie litery, waga trafia w tonach, a operator warunkowy ? : przypisuje kategorię. Jedna przestroga: jeśli callback w map niczego nie zwróci, dostaniesz tablicę pełną undefined. Do samych efektów ubocznych, takich jak wpis w konsoli, służy forEach.

Array.filter() - selekcja danych

filter tworzy nową tablicę z elementami, które spełniają warunek (predykat). To jak filtrowanie próbek DNA - zostawiasz tylko te, które przechodzą kontrolę jakości.

Predykat to funkcja zwracająca true albo false. Przepuśćmy przez dwa filtry stado czterech dinozaurów:

1const allDinosaurs = [
2  { name: 'Rex', diet: 'carnivore', health: 95 },
3  { name: 'Brachio', diet: 'herbivore', health: 88 },
4  { name: 'Raptor', diet: 'carnivore', health: 42 },
5  { name: 'Stego', diet: 'herbivore', health: 100 },
6];
7
8// Filtrowanie - tylko zdrowe dinozaury
9const healthy = allDinosaurs.filter((dino) => dino.health > 80);
10// [Rex, Brachio, Stego]
11
12// Filtrowanie - drapieżniki potrzebujące pomocy
13const sickCarnivores = allDinosaurs.filter(
14  (dino) => dino.diet === 'carnivore' && dino.health < 50
15);
16// [Raptor]

Pierwszy filtr zostawia trzy okazy, drugi, łączący dwa warunki operatorem &&, tylko Raptora. filter zawsze zwraca tablicę, nawet z jednym elementem lub pustą, nigdy pojedyncze true: [10, 20, 30].filter((x) => x > 15) daje [20, 30]. Obiekty nie są przy tym kopiowane - healthy[0] to ten sam obiekt co allDinosaurs[0].

Array.reduce() - agregacja danych

reduce przetwarza tablicę do pojedynczej wartości, kumulując wynik element po elemencie. To jak raport podsumowujący - zbiera dane ze wszystkich wybiegów i tworzy jedno podsumowanie.

Metoda przyjmuje dwa argumenty: funkcję łączącą i wartość początkową. Funkcja dostaje akumulator (dotychczasowy wynik) oraz bieżący element i zwraca nowy akumulator:

1const parkData = [
2  { zone: 'A', dinosaurs: 5, incidents: 0 },
3  { zone: 'B', dinosaurs: 3, incidents: 2 },
4  { zone: 'C', dinosaurs: 8, incidents: 1 },
5];
6
7// Suma wszystkich dinozaurów
8const totalDinos = parkData.reduce(
9  (sum, zone) => sum + zone.dinosaurs, 0
10);
11// 16

Akumulator startuje od 0, a potem rośnie: 0 + 5 = 5, 5 + 3 = 8, 8 + 8 = 16. Wartość początkową podawaj zawsze. Bez niej reduce weźmie pierwszy element jako akumulator (tutaj cały obiekt strefy A, więc wynik byłby bezsensownym tekstem), a na pustej tablicy rzuci TypeError.

Akumulatorem może być również obiekt. Grupujemy strefy według bezpieczeństwa:

1// Grupowanie stref według bezpieczeństwa
2const grouped = parkData.reduce((groups, zone) => {
3  const key = zone.incidents === 0 ? 'safe' : 'risky';
4  return { ...groups, [key]: [...(groups[key] || []), zone.zone] };
5}, {});
6// { safe: ['A'], risky: ['B', 'C'] }

Zapis [key]: to obliczana nazwa właściwości: klucz bierzemy z wartości zmiennej key, czyli 'safe' albo 'risky'. Wyrażenie groups[key] || [] zwraca istniejącą listę albo pustą tablicę, gdy grupy jeszcze nie ma. Każdy krok buduje nowy obiekt grup zamiast zmieniać poprzedni. W nowszych środowiskach (ES2024) jest też gotowe Object.groupBy, ale warto rozumieć, co robi pod spodem.

Łączenie map, filter i reduce

Prawdziwa siła FP objawia się, gdy łączymy te funkcje w pipeline przetwarzania. filter i map zwracają tablice, więc na wyniku od razu wywołujemy kolejną metodę:

1const specimens = [
2  { id: 'S001', species: 'T-Rex', viability: 0.95, cost: 50000 },
3  { id: 'S002', species: 'Raptor', viability: 0.3, cost: 20000 },
4  { id: 'S003', species: 'Brachio', viability: 0.88, cost: 80000 },
5  { id: 'S004', species: 'Stego', viability: 0.72, cost: 35000 },
6  { id: 'S005', species: 'Raptor', viability: 0.91, cost: 22000 },
7];
8
9// Pipeline: filtruj viability > 0.7 -> oblicz koszt -> zsumuj
10const totalViableCost = specimens
11  .filter((s) => s.viability > 0.7)
12  .map((s) => s.cost * s.viability)
13  .reduce((total, cost) => total + cost, 0);
14// 47500 + 70400 + 25200 + 20020 = 163120

Filtr odrzuca tylko próbkę S002, map zamienia cztery pozostałe na koszt ważony żywotnością, a reduce zwija je w jedną liczbę. Kolejność ma znaczenie: najpierw filtruj, potem przekształcaj, żeby map nie liczył niczego dla odrzuconych próbek. A gdy reduce rozrasta się w kilkanaście linii, rozbij je na mniejsze kroki - czytelność jest ważniejsza niż spryt. W następnej lekcji taki łańcuch zamienisz na nazwany pipeline z pipe.

Gdy następnym razem staniesz przed tablicą danych, pomyśl o trzech maszynach laboratorium - filter wybiera próbki spełniające warunek, map przekształca każdą z nich, a reduce zwija wszystko w jeden raport - i żadna z nich nie tknie oryginalnej tablicy.

Kod do tej lekcji: index.js
1// Funkcje wyzszego rzedu: map, filter, reduce
2console.log("=== Park Jurajski - Przetwarzanie Danych DNA ===\n");
3
4const specimens = [
5  { id: "S001", name: "Rex", species: "T-Rex", weight: 8000, diet: "carnivore", health: 95 },
6  { id: "S002", name: "Blue", species: "Velociraptor", weight: 150, diet: "carnivore", health: 88 },
7  { id: "S003", name: "Brachio", species: "Brachiosaurus", weight: 56000, diet: "herbivore", health: 100 },
8  { id: "S004", name: "Stego", species: "Stegosaurus", weight: 5000, diet: "herbivore", health: 72 },
9  { id: "S005", name: "Raptor", species: "Velociraptor", weight: 140, diet: "carnivore", health: 42 },
10  { id: "S006", name: "Trike", species: "Triceratops", weight: 9000, diet: "herbivore", health: 91 },
11];
12
13// MAP - transformacja danych
14console.log("--- map: Raporty wagowe ---");
15const weightReports = specimens.map((s) => ({
16  name: s.name,
17  weightInTons: (s.weight / 1000).toFixed(1) + "t",
18  category: s.weight > 5000 ? "LARGE" : "SMALL",
19}));
20weightReports.forEach((r) => console.log(`  ${r.name}: ${r.weightInTons} (${r.category})`));
21
22// FILTER - selekcja danych
23console.log("\n--- filter: Zdrowe drapiezniki ---");
24const healthyCarnivores = specimens.filter(
25  (s) => s.diet === "carnivore" && s.health > 80
26);
27healthyCarnivores.forEach((s) => console.log(`  ${s.name}: health=${s.health}`));
28
29console.log("\n--- filter: Dinozaury wymagajace leczenia ---");
30const needCare = specimens.filter((s) => s.health < 80);
31needCare.forEach((s) => console.log(`  ${s.name}: health=${s.health}`));
32
33// REDUCE - agregacja
34console.log("\n--- reduce: Statystyki ---");
35const totalWeight = specimens.reduce((sum, s) => sum + s.weight, 0);
36console.log("Laczna waga:", totalWeight, "kg");
37
38const avgHealth = specimens.reduce((sum, s) => sum + s.health, 0) / specimens.length;
39console.log("Srednie zdrowie:", avgHealth.toFixed(1));
40
41// Grupowanie z reduce
42const byDiet = specimens.reduce((groups, s) => {
43  const key = s.diet;
44  return { ...groups, [key]: [...(groups[key] || []), s.name] };
45}, {});
46console.log("Wedlug diety:", byDiet);
47
48// PIPELINE - lancuch operacji
49console.log("\n--- Pipeline: Koszt utrzymania zdrowych duzych dinozaurow ---");
50const maintenanceCost = specimens
51  .filter((s) => s.health > 80)         // tylko zdrowe
52  .filter((s) => s.weight > 1000)        // tylko duze
53  .map((s) => ({ name: s.name, cost: s.weight * 2 })) // koszt = 2x waga
54  .reduce((total, s) => {
55    console.log(`  ${s.name}: $${s.cost}`);
56    return total + s.cost;
57  }, 0);
58console.log("TOTAL:", "$" + maintenanceCost);

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Co robi metoda Array.map() w JavaScript?

  2. 2. Jaki wynik zwróci [1,2,3,4,5].filter(x => x > 3)?

Zadania praktyczne w grze

  • Edytor kodu

    Uzupełnij funkcje calculateThreatIndex, classifyThreat i createReport tak, aby były czyste.

  • Klikanie w kolejności

    Ułóż elementy wywołania reduce sumującego tablicę:

  • Edytor kodu

    Użyj map() do ekstrakcji nazw, filter() do selekcji, reduce() do agregacji i połącz je w pipeline.

Przydatne artykuły