W poprzednich ćwiczeniach nauczyliśmy się pobierać dane z serwerów API za pomocą Fetch i tworzyć zaawansowane klienty HTTP dla naszego Parku Jurajskiego. Teraz przyszedł czas, aby nauczyć się efektywnie przetwarzać, transformować i zarządzać złożonymi strukturami danych JSON, które otrzymujemy z naszych systemów.
JSON (JavaScript Object Notation) to standardowy format wymiany danych, który jest lekki, łatwy do odczytania przez ludzi i prosty do przetwarzania przez maszyny. W Parku Jurajskim używamy JSON do reprezentowania wszystkiego - od danych dinozaurów po statusy systemów bezpieczeństwa i dane odwiedzających.
JSON w JavaScript jest reprezentowany przez dwa główne typy struktur:
Przykładowa struktura JSON reprezentująca dinozaura w naszym parku:
1const trex = {
2 "id": "dino-001",
3 "nazwa": "Rexy",
4 "gatunek": "Tyrannosaurus Rex",
5 "wiek": 25,
6 "statystyki": {
7 "waga": 8000,
8 "dlugosc": 12.3,
9 "wysokosc": 3.6
10 },
11 "diety": ["mięso", "padlina"],
12 "zagrożenie": 5,
13 "lokalizacja": {
14 "wybieg": "T-Rex Kingdom",
15 "koordynaty": {
16 "x": 34.5,
17 "y": 67.2
18 }
19 },
20 "zachowania": [
21 {
22 "typ": "polowanie",
23 "czestosc": "wysoka",
24 "pory": ["rano", "wieczór"]
25 },
26 {
27 "typ": "odpoczynek",
28 "czestosc": "średnia",
29 "pory": ["południe"]
30 }
31 ],
32 "dataDodania": "2023-06-15T08:30:00Z",
33 "aktywny": true
34}JavaScript oferuje dwie podstawowe metody do pracy z JSON:
JSON.parse() - konwertuje string JSON na obiekt JavaScriptJSON.stringify() - konwertuje obiekt JavaScript na string JSON1// Serializacja obiektu do stringa JSON
2const trexString = JSON.stringify(trex);
3console.log("Zserializowany T-Rex:", trexString);
4
5// Parsowanie stringa JSON z powrotem do obiektu
6const parsedTrex = JSON.parse(trexString);
7console.log("Sparsowany T-Rex:", parsedTrex);JSON.stringify() ma dodatkowe parametry, które mogą być przydatne:1// Formatowanie JSON z wcięciami dla czytelności
2const prettyTrex = JSON.stringify(trex, null, 2);
3console.log("Sformatowany T-Rex:");
4console.log(prettyTrex);
5
6// Selektywne serializowanie tylko wybranych pól
7const simpleTrex = JSON.stringify(trex, ['id', 'nazwa', 'gatunek', 'wiek']);
8console.log("Uproszczony T-Rex:", simpleTrex);
9
10// Własna funkcja zastępcza (replacer)
11const customTrex = JSON.stringify(trex, (key, value) => {
12 // Ukrywamy dokładne współrzędne z powodów bezpieczeństwa
13 if (key === 'koordynaty') return "UKRYTE";
14 // Kody poziomu zagrożenia konwertujemy na opis tekstowy
15 if (key === 'zagrożenie') {
16 const opisy = ["niegroźny", "niski", "umiarkowany", "wysoki", "ekstremalny"];
17 return opisy[value - 1] || value;
18 }
19 return value;
20});
21console.log("T-Rex z niestandardową serializacją:", customTrex);Załóżmy, że otrzymaliśmy złożoną strukturę JSON zawierającą dane o wszystkich dinozaurach w parku. Jak możemy efektywnie przetwarzać takie dane?
Filtrowanie to jedna z najczęstszych operacji podczas pracy z kolekcjami danych:
1// Załóżmy, że mamy tablicę dinozaurów
2const dinozaury = await api.get('/dinozaury');
3
4// Filtrowanie tylko drapieżników
5const drapiezniki = dinozaury.filter(dino => dino.diety.includes('mięso'));
6
7// Filtrowanie dinozaurów w określonym wybiegu
8const dinoWWybiegu = dinozaury.filter(dino =>
9 dino.lokalizacja.wybieg === 'T-Rex Kingdom'
10);
11
12// Filtrowanie niebezpiecznych dinozaurów (poziom zagrożenia > 3)
13const niebezpieczneDino = dinozaury.filter(dino => dino.zagrożenie > 3);
14
15// Złożone filtrowanie z wieloma warunkami
16const niebezpieczneDrapieznikiWZonie = dinozaury.filter(dino =>
17 dino.zagrożenie > 3 &&
18 dino.diety.includes('mięso') &&
19 dino.lokalizacja.wybieg === 'Zone A'
20);Często potrzebujemy przekształcić dane z jednego formatu na inny, np. do wyświetlenia na interfejsie użytkownika:
1// Tworzymy uproszczoną listę dinozaurów dla UI
2const prosteDino = dinozaury.map(dino => ({
3 id: dino.id,
4 nazwa: dino.nazwa,
5 gatunek: dino.gatunek,
6 zagrożenie: dino.zagrożenie
7}));
8
9// Dodajemy nowe pola na podstawie istniejących danych
10const dinoZDodatkowymiInformacjami = dinozaury.map(dino => ({
11 ...dino, // kopiujemy wszystkie istniejące pola
12 wiekLat: dino.wiek, // przemianowanie pola
13 zagrozenieOpis: ['niegroźny', 'niski', 'umiarkowany', 'wysoki', 'ekstremalny'][dino.zagrożenie - 1],
14 waga_tony: dino.statystyki.waga / 1000,
15 wymaga_ochrony: dino.zagrożenie > 3
16}));
17
18// Transformacja do specyficznego formatu dla wykresu
19const daneDoWykresu = dinozaury.map(dino => ({
20 x: dino.statystyki.dlugosc,
21 y: dino.statystyki.waga,
22 r: dino.zagrożenie * 5, // rozmiar punktu zależny od zagrożenia
23 label: dino.nazwa,
24 color: dino.diety.includes('mięso') ? 'red' : 'green'
25}));Redukcja pozwala na agregowanie danych w celu uzyskania podsumowań i statystyk:
1// Obliczenie średniej wagi wszystkich dinozaurów
2const sredniWaga = dinozaury.reduce((suma, dino) => suma + dino.statystyki.waga, 0) / dinozaury.length;
3
4// Znalezienie najdłuższego dinozaura
5const najdluzszy = dinozaury.reduce((najdluzszy, dino) =>
6 dino.statystyki.dlugosc > najdluzszy.statystyki.dlugosc ? dino : najdluzszy
7, dinozaury[0]);
8
9// Grupowanie dinozaurów według gatunków
10const dinoPoGatunkach = dinozaury.reduce((grupy, dino) => {
11 // Jeśli grupa dla tego gatunku nie istnieje, tworzymy ją
12 if (!grupy[dino.gatunek]) {
13 grupy[dino.gatunek] = [];
14 }
15 // Dodajemy dinozaura do odpowiedniej grupy
16 grupy[dino.gatunek].push(dino);
17 return grupy;
18}, {});
19
20// Liczenie dinozaurów według poziomu zagrożenia
21const liczbaPoZagrozeniu = dinozaury.reduce((licznik, dino) => {
22 licznik[dino.zagrożenie] = (licznik[dino.zagrożenie] || 0) + 1;
23 return licznik;
24}, {});Metody tablicowe można łączyć w łańcuchy, co pozwala na pisanie zwięzłego i czytelnego kodu:
1// Znajdź 3 najniebezpieczniejsze dinozaury w Zone A
2const top3NiebezpieczneWZoneA = dinozaury
3 .filter(dino => dino.lokalizacja.wybieg === 'Zone A')
4 .sort((a, b) => b.zagrożenie - a.zagrożenie) // sortowanie malejąco
5 .slice(0, 3) // weź tylko pierwsze 3
6 .map(dino => ({
7 nazwa: dino.nazwa,
8 gatunek: dino.gatunek,
9 zagrożenie: dino.zagrożenie
10 }));
11
12// Oblicz średnią wagę dla każdego gatunku
13const sredniaWagaPoGatunkach = Object.entries(
14 dinozaury.reduce((grupy, dino) => {
15 if (!grupy[dino.gatunek]) {
16 grupy[dino.gatunek] = { suma: 0, liczba: 0 };
17 }
18 grupy[dino.gatunek].suma += dino.statystyki.waga;
19 grupy[dino.gatunek].liczba += 1;
20 return grupy;
21 }, {})
22).map(([gatunek, { suma, liczba }]) => ({
23 gatunek,
24 sredniaWaga: suma / liczba
25}));W złożonych strukturach często musimy znaleźć lub zaktualizować głęboko zagnieżdżone dane. Oto kilka podejść:
1// Znajdowanie dinozaura na podstawie głęboko zagnieżdżonego warunku
2const dinoWPobliżuKoordynatów = dinozaury.find(dino => {
3 const { x, y } = dino.lokalizacja.koordynaty;
4 // Szukamy dinozaura w promieniu 5 jednostek od punktu (30, 70)
5 return Math.sqrt(Math.pow(x - 30, 2) + Math.pow(y - 70, 2)) < 5;
6});
7
8// Aktualizacja głęboko zagnieżdżonej właściwości (immutability)
9function updateDinoLocation(dino, newX, newY) {
10 return {
11 ...dino,
12 lokalizacja: {
13 ...dino.lokalizacja,
14 koordynaty: {
15 ...dino.lokalizacja.koordynaty,
16 x: newX,
17 y: newY
18 }
19 }
20 };
21}
22
23// Użycie
24const updatedDino = updateDinoLocation(trex, 35.0, 68.0);Niektóre struktury danych są hierarchiczne lub mają nieznany poziom zagnieżdżenia. W takich przypadkach przydaje się przetwarzanie rekurencyjne:
1// Przykładowa hierarchiczna struktura kategorii dinozaurów
2const kategorieDinozaurów = {
3 "nazwa": "Dinozaury",
4 "podkategorie": [
5 {
6 "nazwa": "Teropody",
7 "podkategorie": [
8 {
9 "nazwa": "Tyranozauroidy",
10 "dinozaury": [
11 { "id": "dino-001", "nazwa": "Rexy" },
12 { "id": "dino-002", "nazwa": "Tara" }
13 ]
14 },
15 {
16 "nazwa": "Deinonychoidy",
17 "dinozaury": [
18 { "id": "dino-003", "nazwa": "Blue" },
19 { "id": "dino-004", "nazwa": "Delta" }
20 ]
21 }
22 ]
23 },
24 {
25 "nazwa": "Zauropody",
26 "dinozaury": [
27 { "id": "dino-005", "nazwa": "Długi" },
28 { "id": "dino-006", "nazwa": "Wysoki" }
29 ]
30 }
31 ]
32};
33
34// Funkcja rekurencyjna do znajdowania dinozaura według ID w hierarchii
35function znajdzDinozauraPoId(wezel, id) {
36 // Sprawdź, czy obecny węzeł ma tablicę dinozaurów
37 if (wezel.dinozaury) {
38 const znaleziony = wezel.dinozaury.find(dino => dino.id === id);
39 if (znaleziony) return znaleziony;
40 }
41
42 // Jeśli węzeł ma podkategorie, przeszukaj je rekurencyjnie
43 if (wezel.podkategorie) {
44 for (const podkategoria of wezel.podkategorie) {
45 const znaleziony = znajdzDinozauraPoId(podkategoria, id);
46 if (znaleziony) return znaleziony;
47 }
48 }
49
50 // Nic nie znaleziono
51 return null;
52}
53
54// Funkcja rekurencyjna do spłaszczania hierarchii i tworzenia listy wszystkich dinozaurów
55function pobierzWszystkieDinozaury(wezel, lista = []) {
56 if (wezel.dinozaury) {
57 lista.push(...wezel.dinozaury);
58 }
59
60 if (wezel.podkategorie) {
61 for (const podkategoria of wezel.podkategorie) {
62 pobierzWszystkieDinozaury(podkategoria, lista);
63 }
64 }
65
66 return lista;
67}
68
69// Użycie
70const blue = znajdzDinozauraPoId(kategorieDinozaurów, "dino-003");
71console.log("Znaleziony dinozaur:", blue);
72
73const wszystkieDino = pobierzWszystkieDinozaury(kategorieDinozaurów);
74console.log("Wszystkie dinozaury:", wszystkieDino);Teraz połączmy wszystkie omówione techniki w jednym praktycznym przykładzie. Załóżmy, że tworzymy panel kontrolny dla kierownika Parku Jurajskiego, który pokazuje status wszystkich dinozaurów w parku.
1// Funkcja pobierająca dane z API
2async function pobierzDaneDinozaurów() {
3 try {
4 const odpowiedź = await fetch('https://api.jurassic-park.com/dinozaury');
5 if (!odpowiedź.ok) throw new Error(`Błąd HTTP: ${odpowiedź.status}`);
6 return await odpowiedź.json();
7 } catch (błąd) {
8 console.error('Błąd pobierania danych:', błąd);
9 // W przypadku błędu zwracamy puste dane
10 return { dinozaury: [] };
11 }
12}
13
14// Funkcja do przetwarzania i przygotowania danych do wyświetlenia
15function przetwórzDaneDoWyświetlenia(dane) {
16 // Pobieranie głównej tablicy dinozaurów
17 const { dinozaury } = dane;
18
19 // Statystyki zbiorcze
20 const statystyki = {
21 liczba: dinozaury.length,
22 liczbaGatunków: new Set(dinozaury.map(d => d.gatunek)).size,
23 średniaWaga: dinozaury.reduce((suma, d) => suma + d.statystyki.waga, 0) / dinozaury.length,
24 maksymalneZagrożenie: Math.max(...dinozaury.map(d => d.zagrożenie))
25 };
26
27 // Grupowanie według wybiegów
28 const dinozauryPoWybiegach = dinozaury.reduce((grupy, dino) => {
29 const wybieg = dino.lokalizacja.wybieg;
30 if (!grupy[wybieg]) grupy[wybieg] = [];
31 grupy[wybieg].push(dino);
32 return grupy;
33 }, {});
34
35 // Liczenie dinozaurów według poziomów zagrożenia
36 const liczbaPoZagrożeniu = [1, 2, 3, 4, 5].map(poziom => ({
37 poziom,
38 liczba: dinozaury.filter(d => d.zagrożenie === poziom).length,
39 procent: (dinozaury.filter(d => d.zagrożenie === poziom).length / dinozaury.length) * 100
40 }));
41
42 // Identyfikacja potencjalnych zagrożeń
43 const zagrożenia = dinozaury
44 .filter(d => d.zagrożenie >= 4)
45 .map(d => ({
46 id: d.id,
47 nazwa: d.nazwa,
48 gatunek: d.gatunek,
49 wybieg: d.lokalizacja.wybieg,
50 zagrożenie: d.zagrożenie,
51 ostatnieZachowanie: d.zachowania[0]?.typ || 'nieznane',
52 koordynaty: d.lokalizacja.koordynaty
53 }))
54 .sort((a, b) => b.zagrożenie - a.zagrożenie);
55
56 // Przygotowanie danych do wykresu wagowego
57 const daneDoWykresuWagi = dinozaury
58 .filter(d => d.statystyki.waga > 0) // Odfiltruj brakujące dane
59 .map(d => ({
60 id: d.id,
61 nazwa: d.nazwa,
62 gatunek: d.gatunek,
63 waga: d.statystyki.waga,
64 drapieżnik: d.diety.includes('mięso'),
65 rozmiarPunktu: Math.sqrt(d.statystyki.waga) / 5
66 }));
67
68 // Ostatnie aktywności (sortowane według daty)
69 const ostatnieAktywności = dinozaury
70 .filter(d => d.zachowania && d.zachowania.length > 0)
71 .flatMap(d => d.zachowania.map(z => ({
72 dinoId: d.id,
73 dinoNazwa: d.nazwa,
74 typ: z.typ,
75 czestosc: z.czestosc,
76 pory: z.pory
77 })))
78 .sort((a, b) => a.dinoId.localeCompare(b.dinoId));
79
80 // Zwróć przetworzone dane
81 return {
82 statystyki,
83 dinozauryPoWybiegach,
84 liczbaPoZagrożeniu,
85 zagrożenia,
86 daneDoWykresuWagi,
87 ostatnieAktywności,
88 surowe: dinozaury // Oryginalne dane, gdyby były potrzebne
89 };
90}
91
92// Funkcja do renderowania danych na interfejsie użytkownika (przykład)
93function wyrenderujPanelKontrolny(przetworzoneDane) {
94 // Tutaj kod aktualizujący widoki UI, wykresy, tabele, mapy, itp.
95 console.log("Przetworzono dane do panelu kontrolnego:", przetworzoneDane);
96
97 // Przykład renderowania tabeli z zagrożeniami
98 const tabelaZagrożeń = document.getElementById('tabela-zagrozen');
99 if (tabelaZagrożeń) {
100 tabelaZagrożeń.innerHTML = przetworzoneDane.zagrożenia
101 .map(z => `
102 <tr>
103 <td>${z.nazwa}</td>
104 <td>${z.gatunek}</td>
105 <td>${z.wybieg}</td>
106 <td class="zagrozenie-${z.zagrożenie}">${z.zagrożenie}</td>
107 <td>${z.ostatnieZachowanie}</td>
108 <td>
109 <button onclick="pokazNaMapie(${z.koordynaty.x}, ${z.koordynaty.y})">
110 Pokaż na mapie
111 </button>
112 </td>
113 </tr>
114 `)
115 .join('');
116 }
117
118 // ... pozostałe elementy UI
119}
120
121// Główna funkcja aktualizacji danych
122async function aktualizujPanel() {
123 // Pobranie danych z API
124 const dane = await pobierzDaneDinozaurów();
125
126 // Przetworzenie danych
127 const przetworzoneDane = przetwórzDaneDoWyświetlenia(dane);
128
129 // Wyrenderowanie panelu
130 wyrenderujPanelKontrolny(przetworzoneDane);
131}
132
133// Uruchomienie aktualizacji co 5 minut
134aktualizujPanel();
135setInterval(aktualizujPanel, 5 * 60 * 1000);W tym ćwiczeniu nauczyliśmy się:
Podstaw serializacji i deserializacji JSON - jak konwertować dane między formatem tekstowym a obiektami JavaScript
Zaawansowanych technik przetwarzania danych:
.filter().map().reduce()Praktycznego zastosowania tych technik w kontekście systemu zarządzania parkiem pełnym dinozaurów
Umiejętność efektywnego przetwarzania i analizy danych JSON jest kluczowa dla każdego współczesnego projektu JavaScript - od prostych stron internetowych po złożone aplikacje jak system zarządzania Parkiem Jurajskim. Dzięki technikom, które poznałeś, będziesz mógł przekształcać surowe dane w użyteczne informacje, które pomogą utrzymać park funkcjonujący sprawnie i bezpiecznie... przynajmniej do czasu, gdy raptor znajdzie lukę w systemie!
W następnym ćwiczeniu przyjrzymy się jak tworzyć i korzystać z Web Workers, aby przenieść intensywne obliczenia (jak analizy zachowań dinozaurów) do oddzielnych wątków, nie blokując głównego interfejsu użytkownika.