`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' ]
pushipopdziałają na końcu tablicy. Są idealne przy implementacji struktur stosu.unshiftishiftoperują 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 ]
slicetylko wyodrębnia elementy i nie modyfikuje oryginalnej tablicy.splicedodaje 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
forjest najbardziej elastyczna – pozwala na operacje na indeksach i dokładne sterowanie iteracją przy użyciubreakczy innych instrukcji. for...ofto zwięzły sposób przetwarzania wartości i najlepszy kompromis między prostotą a czytelnością.forEachpozwala 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ćbreakanicontinue, a także nie nadaje się do asynchronicznego przetwarzania zawait.
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
findzwraca pierwszy element spełniający warunek.findIndexzwraca indeks elementu spełniającego warunek.somezwracatrue, jeśli przynajmniej jeden element spełnia warunek.everyzwracatrue, 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
aprzedb, dodatnia –bprzeda, 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
includesjest 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
concatjest 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ż
flatpozwala 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
joinjest 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
slicelub operator rozproszenia itd.) tworzy płytką kopię. Jeśli Twoje tablice zawierają obiekty, zachowaj ostrożność, bo oryginalne dane mogą zostać przypadkowo zmienione. spliceto destrukcyjna metoda, która bezpośrednio zmienia tablicę, podczas gdysliceto metoda niedestrukcyjna, nie modyfikująca oryginalnej tablicy. Ważne jest, aby używać ich odpowiednio do celu.forEachnie nadaje się do pętli o asynchronicznym przetwarzaniu korzystającym zawait. 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,filterimap, 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.