Tablice

Tablice (arrays) służą do przechowywania uporządkowanych kolekcji danych. W JavaScript tablica jest obiektem specjalnego rodzaju, który przechowuje wartości w indeksowanych (ponumerowanych) pozycjach. Indeksy w tablicy zaczynają się od 0 (pierwszy element ma indeks 0, drugi indeks 1, itd.). Tablice w JavaScript mogą zawierać elementy różnych typów (np. liczby, stringi, a nawet inne tablice czy obiekty), choć często dla przejrzystości przechowuje się w jednej tablicy dane jednego rodzaju. W tej sekcji omówimy tworzenie tablic, dostęp do ich elementów, iterację oraz najważniejsze metody do operacji na tablicach. Pokażemy też, na czym polega destrukturyzacja tablic.

Tworzenie tablic i dostęp do elementów

Tablicę najłatwiej utworzyć za pomocą literału tablicy, czyli elementów ujętych w nawiasy kwadratowe []. Przykład deklaracji tablicy i odczytu jej elementów:

 const liczby = [10, 20, 30, 40];    // tablica liczb
 console.log(liczby[0]);            // 10 (pierwszy element, indeks 0)
 console.log(liczby[3]);            // 40 (czwarty element, indeks 3)
 console.log(liczby.length);        // 4  (właściwość length oznacza liczbę elementów tablicy)

W powyższym przykładzie liczby to tablica zawierająca cztery liczby. Używając nazwaTablicy[indeks] dostajemy się do konkretnego elementu. Należy pamiętać, że:

  • Jeśli spróbujemy odczytać element pod nieistniejącym indeksem (np. liczby[10] w powyższej tablicy), otrzymamy undefined, ponieważ taki element nie istnieje.
  • Właściwość length zawsze zwraca nominalną długość tablicy (ostatni indeks + 1). Można ją też ustawiać (przycięcie lub rozszerzenie tablicy), ale zazwyczaj odczytujemy length, by iterować po tablicy.

Możliwe jest również utworzenie tablicy za pomocą konstruktora Array (np. const arr = new Array(5) tworzy tablicę o długości 5 z pustymi miejscami), ale literał [] jest bardziej powszechny i zalecany.

Elementy tablicy mogą być dowolnego typu, np.:

 const rozne = [42, "tekst", true, null, {x:1, y:2}];
 console.log(rozne[1]);    // "tekst"
 console.log(rozne[4].x);  // 1 (dostęp do pola obiektu wewnątrz tablicy)

Powyższa tablica rozne zawiera kolejno: liczbę, napis, wartość boolean, null oraz obiekt. Widać, że do obiektu wewnątrz tablicy można się dostać najpierw przez indeks tablicy, a następnie przez właściwość obiektu (rozne[4].x).

Iteracja po tablicach

Bardzo często potrzebujemy przejść przez wszystkie elementy tablicy i coś z nimi zrobić (np. wypisać, przetworzyć). Istnieje kilka sposobów iterowania po tablicach:

 const dane = [5, 10, 15];

 // 1. Klasyczna pętla for z indeksem
 for (let i = 0; i < dane.length; i++) {
      console.log("Indeks", i, "wartość", dane[i]);
 }

 // 2. Pętla for...of (ES6) – iteruje bezpośrednio po wartościach
 for (const element of dane) {
      console.log("Wartość:", element);
 }

 // 3. Metoda forEach – wywołuje funkcję dla każdego elementu
 dane.forEach((element, index) => {
      console.log("Index:", index, "wartość:", element);
 });

Wynik działania każdego z powyższych sposobów będzie podobny – w konsoli pojawią się wartości 5, 10, 15 (wraz z indeksami w przykładach 1 i 3). Omówienie sposobów:

  • Pętla for z licznikiem – tradycyjny sposób znany z wielu języków. Dajemy pełną kontrolę nad indeksem, można iterację rozpoczynać/kończyć w dowolnym miejscu, przeskakiwać co kilka itp. Trzeba jednak uważać, by poprawnie zainicjować i aktualizować licznik oraz by warunek i < dane.length był prawidłowy (łatwo o błąd „out of range”).
  • Pętla for…of – nowszy, bardziej zwięzły sposób iteracji. Przy każdej iteracji zmienna (element w powyższym przykładzie) przyjmuje kolejną wartość z tablicy. Nie mamy bezpośrednio dostępu do indeksu (choć można go śledzić osobno lub użyć metody entries()), ale kod jest czytelniejszy, gdy indeks nie jest potrzebny.
  • forEach – metoda tablicowa przyjmująca funkcję zwrotną (callback). Dla każdego elementu tablicy wywoła tę funkcję, przekazując element, indeks i całą tablicę jako argumenty. W przykładzie użyliśmy funkcji strzałkowej, która wypisuje indeks i wartość. forEach jest wygodny, gdy chcemy wykonać operację dla każdego elementu, nie interesuje nas przerwanie iteracji (nie można użyć break wewnątrz forEach tak jak w pętli for) i nie potrzebujemy wyniku (forEach zawsze zwraca undefined).

Istnieje także pętla for…in, jednak w przypadku tablic nie jest zalecana – iteruje ona po właściwościach obiektu, a tablica to obiekt, którego „właściwościami” są indeksy i ewentualnie inne dodane właściwości. Może to dać nieoczekiwane wyniki lub złą kolejność dla tablic, dlatego do tablic preferujemy for, for…of lub metody wbudowane.

Popularne metody tablicowe

JavaScript dostarcza bogaty zestaw metod ułatwiających pracę z tablicami. Poniżej omówimy kilka często używanych metod:

  • push – dodaje element na koniec tablicy. Zwraca nową długość tablicy.
    Przykład: const arr = [1,2]; arr.push(3); // arr teraz [1,2,3].
  • pop – usuwa ostatni element tablicy i zwraca go.
    Przykład: arr.pop(); // zwraca 3, arr staje się [1,2].
  • unshift – dodaje element na początek tablicy (przesuwając istniejące elementy w prawo). Zwraca nową długość.
    Przykład: arr.unshift(0); // arr teraz [0,1,2].
  • shift – usuwa pierwszy element tablicy i zwraca go (przesuwa pozostałe w lewo).
    Przykład: arr.shift(); // zwraca 0, arr staje się [1,2].
  • indexOf – wyszukuje podany element i zwraca jego indeks lub -1 jeśli nie znajdzie.
    Przykład: [10,20,30].indexOf(20); // 1.
  • includes – zwraca true/false czy dany element znajduje się w tablicy.
    Przykład: [10,20,30].includes(25); // false.
  • slice – zwraca nową tablicę będącą wycinkiem oryginalnej (nie zmienia oryginału). Przyjmuje indeks początkowy i końcowy (nie włącznie) wycinka.
    Przykład: const a = [1,2,3,4]; const b = a.slice(1,3); // b = [2,3].
  • splice – uniwersalna metoda do modyfikowania tablicy: może usunąć, dodać lub zastąpić elementy w środku tablicy. Zwraca tablicę usuniętych elementów.
    Przykład: const a = [1,2,3,4]; a.splice(1,2,9,8); // a = [1,9,8,4] (pod indeksem 1 usuwa 2 elementy i wstawia 9,8).
  • forEach – omówiony wyżej; wykonuje przekazaną funkcję dla każdego elementu (nie zwraca nowej tablicy).
  • map – tworzy nową tablicę, w której każdy element jest wynikiem wywołania przekazanej funkcji na odpowiadającym elemencie wejściowym.
    Przykład: [1,2,3].map(x => x * 2); // wynik: [2,4,6] (oryginalna tablica nie zmieniona).
  • filter – tworzy nową tablicę z tymi elementami, dla których przekazana funkcja zwróciła wartość prawdziwą (true).
    Przykład: [1,2,3,4].filter(x => x % 2 === 0); // [2,4] (wyfiltrowano liczby parzyste).
  • reduce – przetwarza tablicę do pojedynczej wartości poprzez wykonywanie funkcji akumulującej na kolejnych elementach. Funkcja ta otrzymuje akumulator i bieżący element, a zwraca nowy akumulator. reduce przyjmuje także wartość początkową akumulatora.
    Przykład (sumowanie): [1,2,3].reduce((acc, x) => acc + x, 0); // 6.
    reduce bywa trudniejszy do zrozumienia na początku, ale jest potężny – pozwala np. sumować, mnożyć, łączyć obiekty, a nawet realizować logikę każdej innej metody (map, filter) w jednym.
  • find – zwraca pierwszy element, dla którego funkcja testująca (callback) zwróci true. Jeśli nie znajdzie, zwraca undefined.
    Przykład: [{x:1},{x:2}].find(obj => obj.x===2); // {x:2}.
  • sort – sortuje tablicę w miejscu (mutuje ją). Domyślnie sortuje elementy jak stringi Unicode, ale można przekazać własną funkcję porównującą do sortowania liczb lub obiektów według klucza.
  • join – łączy wszystkie elementy tablicy w jeden string, z podanym separatorem.
    Przykład: [\"A\",\"B\",\"C\"].join(\"-\"); // \"A-B-C\".

To tylko kilka metod – jest ich więcej, ale te należą do najczęściej używanych. Ważne jest rozróżnienie, które metody mutują oryginalną tablicę (np. push, pop, shift, unshift, splice, sort) a które zwracają nową tablicę pozostawiając oryginał bez zmian (map, filter, slice, etc.).

Przykładowo, jeśli chcemy zachować oryginalne dane, lepiej użyć slice niż splice albo filter zamiast usuwać elementy ręcznie. Natomiast gdy chcemy dokonać zmiany w miejscu (in-place), np. dodając element, użyjemy push/unshift lub usuwając pop/shift.

Destrukturyzacja tablic

Destrukturyzacja (destructuring) to wygodna składnia umożliwiająca wyciąganie wartości z tablic (lub obiektów) i przypisywanie ich do zmiennych, wszystko w jednej deklaracji. W przypadku tablic używamy do tego nawiasów kwadratowych po lewej stronie przypisania.

Przykład destrukturyzacji tablicy:

 const coords = [100, 200, 300];
 const [x, y, z] = coords;
 console.log(x); // 100
 console.log(y); // 200
 console.log(z); // 300

Tutaj tablica coords zawiera trzy liczby. Dzięki składni const [x, y, z] = coords; trzy nowe zmienne x, y, z otrzymują wartości kolejnych elementów tablicy coords. Jest to równoważne trzem osobnym przypisaniom z użyciem indeksów, ale zapisane zwięźle w jednej linii.

Kilka przydatnych możliwości destrukturyzacji:

  • Można zignorować niektóre elementy, wstawiając pusty przecinek. Np.
    const [pierwszy, , trzeci] = [10, 20, 30]; // pomijamy drugi
    Po tym przypisaniu pierwszy == 10, trzeci == 30. Drugi element tablicy został pominięty.
  • Można przypisać tylko pierwsze kilka elementów do zmiennych, a resztę zgrupować w tablicę za pomocą operatora rest ....
    Np.
 const [head, ...tail] = [1, 2, 3, 4, 5]; 
 console.log(head); // 1 console.log(tail); // [2, 3, 4, 5] Tutaj `head` to pierwszy element, a `tail` to tablica z pozostałymi.

Destrukturyzacja jest przydatna przy zamianie wartości dwóch zmiennych bez użycia zmiennej pomocniczej:

 let a = 1, b = 2;  [a, b] = [b, a];    console.log(a, b); // 2 1 Powyżej w jednej linii zamieniliśmy wartości zmiennych `a` i `b`.

Destrukturyzacja zwiększa czytelność, gdy wyciągamy wiele wartości z tablicy (np. wynik funkcji zwracającej tablicę) – zamiast pisać const val0 = arr[0]; const val1 = arr[1]; ..., możemy to zrobić zwięźle.

Przykładowe operacje na tablicach

Aby zademonstrować praktyczną pracę z tablicami, rozważmy następujący przykład: Mamy tablicę liczb i chcemy z tej tablicy wybrać liczby parzyste, następnie podwoić te wartości i na końcu zsumować wszystkie otrzymane wyniki. Możemy to osiągnąć łańcuchowo za pomocą metod filter, map i reduce:

 const liczby = [1, 2, 3, 4, 5];
 const parzyste = liczby.filter(num => num % 2 === 0);      // wybieramy parzyste -> [2, 4]
 const podwojone = parzyste.map(num => num * 2);            // podwajamy każdy -> [4, 8]
 const suma = podwojone.reduce((acc, num) => acc + num, 0); // sumujemy -> 12
 console.log("Suma podwojonych parzystych:", suma);

Omówienie krok po kroku:

  1. Filter: parzyste zawiera wynik liczby.filter(...). Funkcja strzałkowa num => num % 2 === 0 pozostawia w tablicy tylko te elementy, dla których warunek „czy liczba jest parzysta” jest spełniony. Oryginalna tablica liczby pozostaje bez zmian.
  2. Map: Na wynikowej tablicy parzystych liczb wywołujemy map, przekazując funkcję mnożącą każdy element przez 2. Otrzymujemy nową tablicę podwojone z przekształconymi wartościami.
  3. Reduce: Następnie redukujemy tablicę podwojone do pojedynczej wartości sumy. Funkcja akumulatora ((acc, num) => acc + num) dodaje kolejne liczby do akumulatora acc, zaczynając od zera (drugi argument reduce to 0 – wartość początkowa akumulatora). Wynik trafia do zmiennej suma.

Na końcu w zmiennej suma otrzymujemy wartość 12, która jest sumą podwojonych liczb parzystych z oryginalnej tablicy. Taki styl programowania (łączenie operacji na tablicach) jest czytelny i często spotykany, zwłaszcza w przetwarzaniu danych. Pozwala opisać co chcemy zrobić z kolekcją danych w kolejnych krokach, bez pisania ręcznych pętli i liczników.