Kurs JavaScript i TypeScript · Moduł 2: Funkcje, tablice i obiekty
Operatory spread i rest
W tej lekcji6
W Parku Jurajskim ciągle trzeba łączyć dane z różnych systemów monitoringu, kopiować zestawy parametrów i obsługiwać funkcje, które dostają raz dwie, a raz dziesięć próbek. Operatory spread (...) i rest (...) to ten sam zapis, czyli trzy kropki, ale pełnią dwie przeciwne role. Spread "rozpakowuje" elementy, a rest "pakuje" je w jedną strukturę. "To system transportowy parku" - mówi Dr. Rex. "Spread rozładowuje kontenery z próbkami na osobne stanowiska, a rest pakuje wiele próbek do jednego kontenera."
Operator spread dla tablic
Spread "rozprowadza" elementy tablicy w miejsce, gdzie oczekiwane są pojedyncze wartości. Tak połączysz listę mięsożerców z listą roślinożerców:
1const carnivores = ['T-Rex', 'Velociraptor', 'Spinosaurus'];
2const herbivores = ['Triceratops', 'Brachiosaurus', 'Stegosaurus'];
3
4// Łączenie tablic
5const allDinos = [...carnivores, ...herbivores];
6console.log(allDinos);
7// ['T-Rex', 'Velociraptor', 'Spinosaurus', 'Triceratops', 'Brachiosaurus', 'Stegosaurus']
8
9// Dodawanie elementów podczas łączenia
10const withNew = ['Pteranodon', ...carnivores, 'Dilophosaurus'];
11console.log(withNew);
12// ['Pteranodon', 'T-Rex', 'Velociraptor', 'Spinosaurus', 'Dilophosaurus']Wynik to jedna płaska tablica, a nie tablica tablic, bo spread wstawia elementy, a nie całe tablice. Zapis [carnivores, herbivores] bez kropek dałby dwa zagnieżdżone kontenery. Obie tablice źródłowe zostają przy tym bez zmian.
Kopiowanie tablicy (shallow copy)
Spread tworzy płytką kopię tablicy, czyli nową tablicę z tymi samymi elementami. Zapis to const copy = [...original]:
1const originalZones = ['A', 'B', 'C'];
2const zonesCopy = [...originalZones];
3
4zonesCopy.push('D');
5console.log(originalZones); // ['A', 'B', 'C'] - oryginał niezmieniony!
6console.log(zonesCopy); // ['A', 'B', 'C', 'D']Uwaga - płytka kopia! Jeśli tablica zawiera obiekty, kopiowane są tylko referencje do nich, a nie same obiekty:
1const dinos = [{ name: 'Rex', health: 100 }];
2const dinosCopy = [...dinos];
3
4dinosCopy[0].health = 50;
5console.log(dinos[0].health); // 50 - oryginał też się zmienił!
6// Bo oba elementy wskazują na ten sam obiekt w pamięciNowy jest tylko zewnętrzny kontener, a zagnieżdżone obiekty nadal współdzielą referencje z oryginałem. Gdy potrzebujesz niezależnej, głębokiej kopii, polecam wbudowaną funkcję structuredClone().
Spread z funkcjami
Spread pozwala też "rozpakować" tablicę do argumentów funkcji, które oczekują osobnych wartości, jak Math.max:
1const temperatures = [36.5, 37.2, 38.1, 35.8, 39.0];
2
3// Zamiast Math.max(36.5, 37.2, 38.1, 35.8, 39.0)
4const maxTemp = Math.max(...temperatures);
5const minTemp = Math.min(...temperatures);
6console.log('Max:', maxTemp); // 39
7console.log('Min:', minTemp); // 35.8Bez kropek Math.max(temperatures) dostałoby jedną tablicę i zwróciło NaN. Konsola wypisze 39, a nie 39.0, bo liczby nie przechowują końcowych zer. Spread działa na każdej wartości iterowalnej, więc [...'DNA'] daje ['D', 'N', 'A'].
Operator spread dla obiektów
Spread działa również z obiektami i "rozpakowuje" pary klucz-wartość do nowego obiektu:
1const baseDino = { species: 'Unknown', health: 100, zone: 'X' };
2
3// Kopiowanie obiektu
4const dinoCopy = { ...baseDino };
5console.log(dinoCopy); // { species: 'Unknown', health: 100, zone: 'X' }
6
7// Nadpisywanie właściwości (ostatnia wygrywa!)
8const rex = { ...baseDino, species: 'T-Rex', zone: 'A' };
9console.log(rex); // { species: 'T-Rex', health: 100, zone: 'A' }
10
11// Łączenie obiektów
12const position = { lat: 12.34, lon: 56.78 };
13const status = { health: 85, alert: false };
14const fullReport = { ...position, ...status, timestamp: Date.now() };
15console.log(fullReport);
16// { lat: 12.34, lon: 56.78, health: 85, alert: false, timestamp: ... }Spread kopiuje własne, wyliczalne właściwości obiektu, także te, których kluczem jest Symbol. baseDino pozostał szablonem, z którego można tworzyć kolejne okazy.
Kolejność ma znaczenie
Przy łączeniu obiektów późniejsze właściwości nadpisują wcześniejsze. Dlatego ustawienia domyślne zawsze rozpakowuj pierwsze:
1const defaults = { voltage: 10000, locked: true, zone: 'A' };
2const userSettings = { voltage: 15000, zone: 'B' };
3
4// Defaults najpierw, potem nadpisanie
5const config = { ...defaults, ...userSettings };
6console.log(config);
7// { voltage: 15000, locked: true, zone: 'B' }
8
9// Odwrotna kolejność - defaults nadpisałyby ustawienia!
10const wrong = { ...userSettings, ...defaults };
11console.log(wrong);
12// { voltage: 10000, zone: 'A', locked: true } - źle!Pozycję klucza wyznacza jego pierwsze pojawienie się, a wartość ostatnie. Dlatego w wrong klucze stoją w innej kolejności, a wygrały wartości domyślne. Tak samo { ...{ a: 1, b: 2 }, ...{ b: 3, c: 4 } } daje { a: 1, b: 3, c: 4 }.
Operator rest w funkcjach
Rest (...) w parametrach funkcji zbiera wszystkie pozostałe argumenty do tablicy. Obserwator może zgłosić dowolną liczbę gatunków:
1// Rest parameter - zbiera zmienną liczbę argumentów
2function logSighting(observer, ...dinosaurs) {
3 console.log(`${observer} zaobserwował: ${dinosaurs.join(', ')}`);
4 console.log(`Liczba gatunków: ${dinosaurs.length}`);
5}
6
7logSighting('Dr. Rex', 'T-Rex', 'Velociraptor', 'Triceratops');
8// "Dr. Rex zaobserwował: T-Rex, Velociraptor, Triceratops"
9// "Liczba gatunków: 3"
10
11logSighting('Dr. Rex', 'Brachio');
12// "Dr. Rex zaobserwował: Brachio"
13// "Liczba gatunków: 1"Pierwszy argument trafia do observer, a cała reszta do prawdziwej tablicy dinosaurs, więc działają na niej join i length. Starszy obiekt arguments tablicą nie jest, dlatego w nowym kodzie polecam rest.
Rest musi być ostatnim parametrem
Rest zbiera wszystko, co zostało, więc musi stać na końcu listy parametrów. Inaczej JavaScript nie wiedziałby, gdzie skończyć zbieranie:
1// Poprawnie - rest na końcu
2function feedDinos(keeper, time, ...dinoNames) {
3 dinoNames.forEach(name => {
4 console.log(`${keeper} karmi ${name} o ${time}`);
5 });
6}
7
8feedDinos('Alan', '08:00', 'Rex', 'Blue', 'Delta');
9
10// Błąd! Rest nie może być przed innymi parametrami
11// function wrong(...names, time) {} // SyntaxError!Funkcja może mieć tylko jeden parametr rest i zawsze jest to ostatni parametr. Każda inna pozycja kończy się błędem SyntaxError jeszcze przed uruchomieniem kodu.
Rest w destrukturyzacji
Rest działa również w destrukturyzacji tablic i obiektów, którą znasz z poprzedniej lekcji:
1// Rest w destrukturyzacji tablicy
2const rankings = ['T-Rex', 'Velociraptor', 'Spinosaurus', 'Giganotosaurus'];
3const [champion, runnerUp, ...others] = rankings;
4console.log(champion); // "T-Rex"
5console.log(runnerUp); // "Velociraptor"
6console.log(others); // ["Spinosaurus", "Giganotosaurus"]
7
8// Rest w destrukturyzacji obiektu
9const sensor = { id: 'S-001', value: 42, unit: 'C', zone: 'A', active: true };
10const { id: sensorId, value: sensorValue, ...metadata } = sensor;
11console.log(sensorId); // "S-001"
12console.log(sensorValue); // 42
13console.log(metadata); // { unit: 'C', zone: 'A', active: true }W obu przypadkach rest tworzy nową tablicę albo nowy obiekt z tym, czego nie wymieniono z nazwy.
Praktyczne wzorce
Usuwanie właściwości z obiektu
Połączenie destrukturyzacji z rest pozwala pominąć pola bez modyfikowania obiektu, na przykład ukryć dane wewnętrzne przed wysłaniem raportu:
1const dino = { name: 'Rex', _secret: 'code', health: 100, _internal: true };
2
3// Wyciągnij to, co chcesz usunąć, reszta zostaje
4const { _secret, _internal, ...publicData } = dino;
5console.log(publicData); // { name: 'Rex', health: 100 }Obiekt dino nadal ma _secret. Wyciągnięte pola stały się osobnymi zmiennymi, a publicData to nowy obiekt bez nich.
Tworzenie kopii z modyfikacją
Spread sprawdza się też przy aktualizacji danych. Zamiast zmieniać oryginał, tworzysz jego nową wersję z nadpisanymi polami:
1function updateDino(dino, updates) {
2 return { ...dino, ...updates, lastModified: Date.now() };
3}
4
5const rex = { name: 'Rex', health: 100, zone: 'A' };
6const updatedRex = updateDino(rex, { health: 85, zone: 'B' });
7console.log(updatedRex);
8// { name: 'Rex', health: 85, zone: 'B', lastModified: ... }
9console.log(rex.health); // 100 - oryginał niezmieniony!Stary rekord zostaje nietknięty, więc łatwo porównać stan przed zmianą i po niej. Na tym wzorcu opiera się aktualizacja stanu w React i Redux.
Praktyka
W piaskownicy czekają łączenie tablic, kopie, Math.max ze spreadem, kolejność przy łączeniu obiektów i rest w funkcjach. Zamień kolejność spreadów i sprawdź, która wartość wygra.
Kod do tej lekcji: index.js
1// === OPERATORY SPREAD I REST ===
2
3// 1. Spread - łączenie tablic
4const carnivores = ['T-Rex', 'Velociraptor', 'Spinosaurus'];
5const herbivores = ['Triceratops', 'Brachiosaurus'];
6const allDinos = [...carnivores, ...herbivores];
7console.log('=== Spread tablice ===');
8console.log('Wszystkie:', allDinos);
9
10// 2. Kopiowanie tablicy (shallow copy)
11const original = ['A', 'B', 'C'];
12const copy = [...original];
13copy.push('D');
14console.log('\n=== Kopiowanie ===');
15console.log('Oryginał:', original);
16console.log('Kopia:', copy);
17
18// 3. Spread z funkcjami
19const temps = [36.5, 37.2, 38.1, 35.8, 39.0];
20console.log('\n=== Spread z funkcjami ===');
21console.log('Max temp:', Math.max(...temps));
22console.log('Min temp:', Math.min(...temps));
23
24// 4. Spread dla obiektów
25const baseDino = { species: 'Unknown', health: 100 };
26const rex = { ...baseDino, species: 'T-Rex', zone: 'A' };
27console.log('\n=== Spread obiekty ===');
28console.log('Rex:', JSON.stringify(rex));
29
30// Łączenie obiektów
31const position = { lat: 12.34, lon: 56.78 };
32const status = { health: 85, alert: false };
33const fullReport = { ...position, ...status };
34console.log('Raport:', JSON.stringify(fullReport));
35
36// 5. Kolejność ma znaczenie
37const defaults = { voltage: 10000, locked: true };
38const settings = { voltage: 15000 };
39console.log('\n=== Kolejność ===');
40console.log('Poprawne:', JSON.stringify({ ...defaults, ...settings }));
41console.log('Złe:', JSON.stringify({ ...settings, ...defaults }));
42
43// 6. Rest w funkcjach
44function logSighting(observer, ...dinosaurs) {
45 console.log(observer + ' widzi: ' + dinosaurs.join(', '));
46 console.log('Liczba: ' + dinosaurs.length);
47}
48console.log('\n=== Rest w funkcjach ===');
49logSighting('Dr. Rex', 'T-Rex', 'Velociraptor', 'Triceratops');
50
51// 7. Rest w destrukturyzacji
52const [champion, runnerUp, ...others] = ['T-Rex', 'Raptor', 'Spino', 'Giga'];
53console.log('\n=== Rest w destrukturyzacji ===');
54console.log('Mistrz:', champion);
55console.log('Reszta:', others);
56
57const { name, ...dinoStats } = { name: 'Rex', weight: 8000, zone: 'A' };
58console.log('Imię:', name);
59console.log('Statystyki:', JSON.stringify(dinoStats));Zapamiętaj: te same trzy kropki po prawej stronie rozładowują kontener, a w parametrach i po lewej stronie przypisania pakują próbki do nowego pojemnika.
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co zwróci [...['A', 'B'], ...['C', 'D']]?
2. Co zwróci { ...{ a: 1, b: 2 }, ...{ b: 3, c: 4 } }?
3. Gdzie musi znajdować się parametr rest (...args) w liście parametrów funkcji?
4. Dlaczego spread tworzy 'płytką kopię' (shallow copy) obiektów?
Zadania praktyczne w grze
- Edytor kodu
Masz tablice const carnivores = ['T-Rex', 'Velociraptor'] i const herbivores = ['Triceratops'] oraz obiekt const defaults = { zone: 'A', locked: true }. Operatorem spread utwórz: allDinos (połączenie obu tablic), carnivoresCopy (nową kopię carnivores) i config (defaults nadpisane polem zone: 'B'). Napisz też funkcję countSightings(observer, ...dinos), która zwraca liczbę przekazanych dinozaurów.
- Układanie w poziomie
Ułóż elementy tworzenia kopii tablicy za pomocą spread:
- Edytor kodu
Masz obiekt const dino = { name: 'Rex', health: 100, _secret: 'code', _internal: true }. Destrukturyzacją z operatorem rest utwórz nowy obiekt publicData bez pól _secret i _internal (dino ma zostać bez zmian). Napisz funkcję updateDino(dino, updates), która zwraca nowy obiekt: kopię dino nadpisaną polami z updates, bez zmieniania przekazanego obiektu.
- Edytor kodu
Utwórz dwa identyfikatory próbek DNA: const sampleId1 = Symbol('DNA-001') i const sampleId2 = Symbol('DNA-001'). Wyświetl w konsoli wynik porównania sampleId1 === sampleId2. Następnie utwórz obiekt specimen z właściwością name: 'T-Rex' i z danymi próbki (np. { purity: 98.5 }) zapisanymi pod kluczem sampleId1.