`Obiekt` Array`

Ten artykuł wyjaśnia, czym jest obiekt Array.

Wyjaśnię praktyczne zastosowania tablic krok po kroku w prosty i zrozumiały sposób.

YouTube Video

Obiekt Array`

Obiekt Array w JavaScript to jedna z najważniejszych struktur stanowiących podstawę wszelkiego przetwarzania danych. Od podstawowych operacji na tablicach aż po funkcje wyższego rzędu użyteczne do wydajnego przekształcania danych – jest wiele funkcji, które warto znać.

Podstawy tablic

W JavaScript tablice to podstawowa struktura danych służąca do przechowywania wielu wartości jednocześnie. Tutaj przedstawiamy, jak tworzyć tablice oraz jak odczytywać i zapisywać ich elementy na prostych przykładach.

 1// Create arrays in different ways
 2const arr1 = [1, 2, 3];           // array literal
 3const arr2 = new Array(4, 5, 6);  // Array constructor
 4const arr3 = Array.of(7, 8, 9);   // Array.of
 5
 6console.log("arr1 created with literal.   :", arr1);
 7console.log("arr2 created with constructor:", arr2);
 8console.log("arr3 created with Array.of.  :", arr3);
 9// arr1 created with literal.   : [ 1, 2, 3 ]
10// arr2 created with constructor: [ 4, 5, 6 ]
11// arr3 created with Array.of.  : [ 7, 8, 9 ]
12
13// Access and modify elements
14let first = arr1[0];              // read element
15console.log("First element of arr1:", first);
16// First element of arr1: 1
17
18arr1[1] = 20;                     // modify element
19console.log("arr1 after modifying index 1:", arr1);
20// arr1 after modifying index 1: [ 1, 20, 3 ]
21
22const len = arr1.length;          // get length
23console.log("Length of arr1:", len);
24// Length of arr1: 3
  • Ten kod pokazuje trzy sposoby tworzenia tablic, jak odczytywać i aktualizować elementy za pomocą indeksów oraz jak uzyskać długość tablicy przez właściwość length.
  • Literały tablic są najbardziej powszechne i czytelne, i są najczęściej używane w codziennych sytuacjach.

Dodawanie i usuwanie elementów (na końcu lub początku tablicy)

Tablice pozwalają łatwo dodawać lub usuwać elementy na końcu lub na początku. Te operacje przydają się również przy implementacji struktur takich jak stosy czy kolejki.

 1// Push and pop (stack-like)
 2const stack = [];
 3console.log("Initial stack:", stack);
 4
 5stack.push(1);                    // push 1
 6console.log("After push(1):", stack);
 7
 8stack.push(2);                    // push 2
 9console.log("After push(2):", stack);
10
11const last = stack.pop();         // pop -> 2
12console.log("Popped value:", last);
13console.log("Stack after pop():", stack);
14
15// Unshift and shift (queue-like)
16const queue = [];
17console.log("Initial queue:", queue);
18
19queue.push('a');                  // add to end
20console.log("After push('a'):", queue);
21
22queue.unshift('start');           // add to front
23console.log("After unshift('start'):", queue);
24
25const firstItem = queue.shift();  // remove from front
26console.log("Shifted value:", firstItem);
27console.log("Queue after shift():", queue);
28
29// Initial stack: []
30// After push(1): [ 1 ]
31// After push(2): [ 1, 2 ]
32// Popped value: 2
33// Stack after pop(): [ 1 ]
34
35// Initial queue: []
36// After push('a'): [ 'a' ]
37// After unshift('start'): [ 'start', 'a' ]
38// Shifted value: start
39// Queue after shift(): [ 'a' ]
  • push i pop działają na końcu tablicy. Są idealne przy implementacji struktur stosu.
  • unshift i shift operują na początku tablicy. Należy jednak pamiętać, że operacje na początku tablicy wymagają przesunięcia wszystkich indeksów, co sprawia, że są kosztowne wydajnościowo.

Obsługa elementów w środku tablicy (splice i slice)

Przy pracy z elementami w środku tablicy wybierz splice lub slice w zależności od tego, czy chcesz modyfikować oryginalną tablicę, czy nie. Jeśli chcesz tylko wyodrębnić część tablicy, użyj slice; jeśli chcesz zmodyfikować samą tablicę, na przykład wstawiając lub usuwając elementy, użyj splice.

 1// slice (non-destructive)
 2const nums = [0, 1, 2, 3, 4];
 3console.log("Original nums:", nums);
 4
 5const part = nums.slice(1, 4); // returns [1, 2, 3]
 6console.log("Result of nums.slice(1, 4):", part);
 7console.log("nums after slice (unchanged):", nums);
 8
 9// splice (destructive)
10const arr = [10, 20, 30, 40];
11console.log("\nOriginal arr:", arr);
12
13// remove 1 item at index 2, insert 25 and 27
14arr.splice(2, 1, 25, 27);
15console.log("After arr.splice(2, 1, 25, 27):", arr);
16
17// Original nums: [ 0, 1, 2, 3, 4 ]
18// Result of nums.slice(1, 4): [ 1, 2, 3 ]
19// nums after slice (unchanged): [ 0, 1, 2, 3, 4 ]
20
21// Original arr: [ 10, 20, 30, 40 ]
22// After arr.splice(2, 1, 25, 27): [ 10, 20, 25, 27, 40 ]
  • slice tylko wyodrębnia elementy i nie modyfikuje oryginalnej tablicy.
  • splice dodaje lub usuwa elementy i modyfikuje samą tablicę, więc należy uważać na wpływ tej funkcji na działanie kodu.

Iteracja (for / for...of / forEach)

Istnieje kilka sposobów przetwarzania tablic po kolei – wybierz odpowiedni według celu oraz preferowanego stylu kodowania. Oto trzy najczęściej używane konstrukcje pętli.

 1const items = ['apple', 'banana', 'cherry'];
 2console.log("Items:", items);
 3
 4// classic for
 5console.log("\n--- Classic for loop ---");
 6for (let i = 0; i < items.length; i++) {
 7  console.log(`Index: ${i}, Value: ${items[i]}`);
 8}
 9
10// for...of (recommended for values)
11console.log("\n--- for...of loop ---");
12for (const item of items) {
13  console.log(`Value: ${item}`);
14}
15
16// forEach (functional style)
17console.log("\n--- forEach loop ---");
18items.forEach((item, index) => {
19  console.log(`Index: ${index}, Value: ${item}`);
20});
  • Pętla for jest najbardziej elastyczna – pozwala na operacje na indeksach i dokładne sterowanie iteracją przy użyciu break czy innych instrukcji.
  • for...of to zwięzły sposób przetwarzania wartości i najlepszy kompromis między prostotą a czytelnością.
  • forEach pozwala pisać kod w stylu funkcyjnym i świetnie sprawdza się przy operacjach powodujących skutki uboczne, takich jak logowanie czy aktualizacja danych elementów. Należy jednak pamiętać, że nie można stosować break ani continue, a także nie nadaje się do asynchronicznego przetwarzania z await.

map / filter / reduce — funkcje wyższego rzędu

map, filter i reduce to funkcje wyższego rzędu, które są często używane podczas przekształcania, filtrowania lub agregowania tablic. Ponieważ możesz jasno wyrazić powtarzające się operacje, twój kod staje się prosty i łatwy do zrozumienia.

 1const numbers = [1, 2, 3, 4, 5];
 2console.log("Original numbers:", numbers);
 3
 4// map: transform each item
 5const doubled = numbers.map(n => n * 2);
 6console.log("\nResult of map (n * 2):", doubled);
 7
 8// filter: select items
 9const evens = numbers.filter(n => n % 2 === 0);
10console.log("Result of filter (even numbers):", evens);
11
12// reduce: accumulate to single value
13const sum = numbers.reduce((acc, n) => acc + n, 0);
14console.log("Result of reduce (sum):", sum);
15
16// Original numbers: [ 1, 2, 3, 4, 5 ]
17// Result of map (n * 2): [ 2, 4, 6, 8, 10 ]
18// Result of filter (even numbers): [ 2, 4 ]
19// Result of reduce (sum): 15
  • Metody te pozwalają skupić się na tym, co chcesz osiągnąć, w deklaratywnym stylu, zwiększając czytelność i pomagając unikać niepożądanych skutków ubocznych.

find / findIndex / some / every

Oto przegląd metod wyszukiwania i sprawdzania warunków. Są one użyteczne do wyszukiwania elementów spełniających określone warunki lub wykonywania sprawdzeń logicznych na zbiorze.

 1const users = [
 2  { id: 1, name: 'Alice' },
 3  { id: 2, name: 'Bob' },
 4  { id: 3, name: 'Carol' }
 5];
 6
 7console.log("Users:", users);
 8
 9// Find the first user whose name is 'Bob'
10const bob = users.find(user => user.name === 'Bob');
11console.log("\nResult of find (name === 'Bob'):", bob);
12
13// Find index of the user whose id is 3
14const indexOfId3 = users.findIndex(user => user.id === 3);
15console.log("Result of findIndex (id === 3):", indexOfId3);
16
17// Check if there exists a user with id = 2
18const hasId2 = users.some(user => user.id === 2);
19console.log("Result of some (id === 2):", hasId2);
20
21// Check if all users have a numeric id
22const allHaveNumericId = users.every(user => typeof user.id === 'number');
23console.log("Result of every (id is number):", allHaveNumericId);
24// Result of find (name === 'Bob'): { id: 2, name: 'Bob' }
25// Result of findIndex (id === 3): 2
26// Result of some (id === 2): true
27// Result of every (id is number): true
  • find zwraca pierwszy element spełniający warunek.
  • findIndex zwraca indeks elementu spełniającego warunek.
  • some zwraca true, jeśli przynajmniej jeden element spełnia warunek.
  • every zwraca true, jeśli wszystkie elementy spełniają warunek.

Wszystkie te metody są bardzo przydatne przy przetwarzaniu tablic, więc stosując je odpowiednio do sytuacji, Twój kod będzie zwięzły i przejrzysty.

Sortowanie i funkcje porównujące

Tablice są sortowane przy użyciu sort, ale domyślnie porównuje on elementy jako ciągi znaków, co może prowadzić do nieoczekiwanych efektów przy sortowaniu liczb.

 1const nums = [10, 2, 33, 4];
 2console.log("Original nums:", nums);
 3
 4// Default sort: compares elements as strings (not suitable for numbers)
 5nums.sort();
 6console.log("\nAfter default sort (string comparison):", nums);
 7
 8// Numeric ascending sort using a compare function
 9nums.sort((a, b) => a - b);
10console.log("After numeric sort (a - b):", nums);
11
12// Sort objects by a property
13const people = [{ age: 30 }, { age: 20 }, { age: 25 }];
14console.log("\nOriginal people:", people);
15
16people.sort((a, b) => a.age - b.age);
17console.log("After sorting people by age:", people);
18
19// After default sort (string comparison): [ 10, 2, 33, 4 ]
20// After numeric sort (a - b): [ 2, 4, 10, 33 ]
21// Original people: [ { age: 30 }, { age: 20 }, { age: 25 } ]
22// After sorting people by age: [ { age: 20 }, { age: 25 }, { age: 30 } ]
  • Przy sortowaniu liczb lub obiektów zawsze podawaj funkcję porównującą, aby uzyskać zamierzony porządek.
  • W funkcji porównującej wartość ujemna umieszcza a przed b, dodatniab przed a, a 0 – pozostawia ich kolejność bez zmian.

Kopiowanie tablic i niezmienność

Kopiując tablice, ważne jest, aby zrozumieć różnicę między 'kopią referencyjną' a 'płytką kopią'. W szczególności należy pamiętać, że jeśli w tablicy znajdują się obiekty, płytka kopia sprawia, że obiekty te będą współdzielone.

Kopia referencyjna

Gdy przypisujesz tablicę do innej zmiennej, sama tablica nie jest duplikowana; kopiowana jest jedynie 'referencja' wskazująca na tę samą tablicę.

 1const a = [1, 2, 3];
 2console.log("Original a:", a);
 3
 4// Reference copy; modifying b also affects a
 5const b = a;
 6console.log("\nReference copy b = a:", b);
 7// Reference copy b = a: [ 1, 2, 3 ]
 8
 9b[0] = 100;
10console.log("After modifying b[0] = 100:");
11console.log("a:", a);  // a: [ 100, 2, 3 ] (affected)
12console.log("b:", b);  // b: [ 100, 2, 3 ]
  • W przypadku kopii referencyjnej, jeśli zmodyfikujesz zawartość tablicy za pomocą skopiowanej zmiennej, te zmiany zostaną również odzwierciedlone w oryginalnej zmiennej odnoszącej się do tej samej tablicy.

Płytka kopia

Użycie slice() lub operatora spread powoduje utworzenie 'płytkiej kopii', ponieważ kopiowane są tylko wartości; oryginalna i skopiowana tablica są traktowane jako oddzielne byty.

 1const a = [1, 2, 3];
 2console.log("Original a:", a);
 3
 4// Shallow copy (new array)
 5const b = a.slice();
 6console.log("\nShallow copy using slice():", b);
 7// Shallow copy using slice(): [ 100, 2, 3 ]
 8
 9const c = [...a];
10console.log("Shallow copy using spread [...a]:", c);
11// Shallow copy using spread [...a]: [ 100, 2, 3 ]
12
13// Modifying c or d does NOT affect a
14b[1] = 200;
15c[2] = 300;
16console.log("\nAfter modifying b[1] = 200 and c[2] = 300:");
17console.log("a:", a);   // [ 100, 2, 3 ]
18console.log("b:", b);   // [ 100, 200, 3 ]
19console.log("c:", c);   // [ 100, 2, 300 ]
  • Ten kod pokazuje, że płytka kopia tablicy utworzona przez slice() lub operator spread nie wpływa na oryginalną tablicę.

Płytka kopia i niemutowalność

Nawet jeśli skopiujesz tablicę za pomocą 'płytkiej kopii', może dojść do niezamierzonego współdzielenia, jeśli tablica zawiera obiekty wewnątrz.

 1// Shallow copy doesn't clone inner objects
 2const nested = [{ x: 1 }, { x: 2 }];
 3console.log("\nOriginal nested:", nested);
 4// Original nested: [ { x: 1 }, { x: 2 } ]
 5
 6const shallow = nested.slice();
 7console.log("Shallow copy of nested:", shallow);
 8// Shallow copy of nested: [ { x: 1 }, { x: 2 } ]
 9
10// Changing inner object affects both arrays
11shallow[0].x = 99;
12console.log("\nAfter shallow[0].x = 99:");
13console.log("nested:", nested);
14console.log("shallow:", shallow);
15// nested: [ { x: 99 }, { x: 2 } ]
16// shallow: [ { x: 99 }, { x: 2 } ]
  • Ten kod pokazuje, że w przypadku płytkiej kopii wewnętrzne obiekty są współdzielone, więc ich modyfikacja wpływa zarówno na oryginalną tablicę, jak i na kopię.
  • Jeśli potrzebujesz niezależnych danych, musisz użyć 'głębokiej kopii', np. przez structuredClone() lub konwersję do/z JSON.

Przydatne metody pomocnicze

Poniższe metody narzędziowe są często używane przy pracy z tablicami. Stosując je odpowiednio do celu, możesz pisać krótki i czytelny kod.

includes

Metoda includes sprawdza, czy w tablicy znajduje się określona wartość.

1// includes: check if array contains a value
2const letters = ['a', 'b', 'c'];
3const hasB = letters.includes('b');
4console.log("letters:", letters);  // [ 'a', 'b', 'c' ]
5console.log("letters.includes('b'):", hasB); // true
  • W tym kodzie metoda includes jest używana do zwięzłego sprawdzenia, czy dana wartość istnieje w tablicy.

concat

Metoda concat zwraca nową tablicę, która dołącza wskazaną tablicę lub wartości na końcu, pozostawiając oryginalną tablicę bez zmian.

1// concat: merge arrays without mutation
2const a1 = [1, 2];
3const a2 = [3, 4];
4const combined = a1.concat(a2);
5console.log("a1.concat(a2):", combined); // [ 1, 2, 3, 4 ]
6console.log("a1(unchanged):", a1);       // [ 1, 2 ]
  • Ten kod pokazuje, że concat jest niedestrukcyjny, umożliwiając utworzenie nowej tablicy przy zachowaniu oryginalnej.

flat

Używając metody flat, możesz spłaszczyć zagnieżdżone tablice.

1// flat and flatMap: flatten arrays or map + flatten in one step
2const nested = [1, [2, [3]]];
3console.log("nested:", nested); // nested: [ 1, [ 2, [ 3 ] ] ]
4console.log("nested.flat():", nested.flat()); // default depth = 1
5// nested.flat(): [ 1, 2, [ 3 ] ]
  • Ten kod pokazuje wynik spłaszczenia tablicy o jeden poziom.
  • Ponieważ flat pozwala określić głębokość, możesz elastycznie rozwiązywać zagnieżdżenia w razie potrzeby.

flatMap

Metoda flatMap stosuje transformację do każdego elementu, a następnie automatycznie spłaszcza wyniki do jednowymiarowej tablicy.

1// flat and flatMap: flatten arrays or map + flatten in one step
2const words = ['hello world', 'hi'];
3console.log("words:", words); // [ 'hello world', 'hi' ]
4
5const splitWords = words.flatMap(w => w.split(' '));
6console.log("words.flatMap(w => w.split(' ')):", splitWords);
7// words.flatMap(w => w.split(' ')): [ 'hello', 'world', 'hi' ]
  • Ten kod przedstawia przykład, w którym każdy ciąg znaków w tablicy jest dzielony według spacji, a wyniki są łączone i spłaszczane do jednej tablicy.

join

Metoda join tworzy ciąg znaków poprzez połączenie elementów tablicy za pomocą określonego separatora.

1// join: combine elements into a string with a separator
2const csv = ['a', 'b', 'c'].join(',');
3console.log("['a', 'b', 'c'].join(','):", csv);  // a,b,c
  • W tym kodzie metoda join jest używana do konwersji tablicy na ciąg rozdzielony przecinkami.

Typowe pułapki

Operacje na tablicach mogą wydawać się proste, ale istnieje kilka pułapek, które łatwo prowadzą do niezamierzonego działania. W codziennej pracy łatwo przeoczyć te pułapki, dlatego pamiętanie o poniższych zasadach znacznie zwiększy niezawodność Twojego kodu.

  • sort() dla tablic domyślnie sortuje przez porównywanie jako ciągi znaków. Przy poprawnym sortowaniu liczb zawsze podawaj funkcję porównującą.
  • Kopiowanie tablic (przez slice lub operator rozproszenia itd.) tworzy płytką kopię. Jeśli Twoje tablice zawierają obiekty, zachowaj ostrożność, bo oryginalne dane mogą zostać przypadkowo zmienione.
  • splice to destrukcyjna metoda, która bezpośrednio zmienia tablicę, podczas gdy slice to metoda niedestrukcyjna, nie modyfikująca oryginalnej tablicy. Ważne jest, aby używać ich odpowiednio do celu.
  • forEach nie nadaje się do pętli o asynchronicznym przetwarzaniu korzystającym z await. Aby niezawodnie obsługiwać asynchroniczne przetwarzanie w kolejności, zaleca się stosować for...of.

Praktyczny przykład

Poniżej znajduje się przykład łączący metody tablicowe w celu 'uzyskania łącznego wieku z danych użytkowników, wyodrębnienia osób mających co najmniej 30 lat i stworzenia listy imion.'.

 1const users = [
 2  { name: 'Alice', age: 28 },
 3  { name: 'Bob', age: 34 },
 4  { name: 'Carol', age: 41 },
 5  { name: 'Dave', age: 19 }
 6];
 7
 8console.log("Users:", users);
 9
10// sum ages
11const totalAge = users.reduce((acc, u) => acc + u.age, 0);
12console.log("\nTotal age of all users:", totalAge);
13// Total age of all users: 122
14
15// filter and map names of users 30 and older
16const namesOver30 = users
17  .filter(u => u.age >= 30)
18  .map(u => u.name);
19
20console.log("Users aged 30 or older (names):", namesOver30);
21// Users aged 30 or older (names): [ 'Bob', 'Carol' ]
  • Łącząc reduce, filter i map, możesz w prosty sposób napisać agregację, wyodrębnianie według warunku i przekształcanie danych.
  • Tego typu 'łańcuch przetwarzania danych' jest bardzo czytelny i – dzięki niewielu skutkom ubocznym – często spotykany w praktycznych zastosowaniach.

Podsumowanie

W JavaScript nawet podstawowe operacje na tablicach są szeroko przydatne, a użycie funkcji wyższego rzędu sprawia, że kod staje się zwięzły i wyrazisty. Do zrozumienia jest wiele zagadnień, ale po opanowaniu ich stosowania przetwarzanie danych stanie się znacznie płynniejsze.

Możesz śledzić ten artykuł, korzystając z Visual Studio Code na naszym kanale YouTube. Proszę również sprawdzić nasz kanał YouTube.

YouTube Video