Kurs JavaScript i TypeScript · Moduł 12: Programowanie funkcyjne
Wzorce niemutowalności danych
W tej lekcji7
Mapa parku i moduł raportów dostają ten sam obiekt stanu. Mapa przesuwa Rexa do strefy B, zmieniając pole bezpośrednio, i nagle raport z porannej zmiany też pokazuje strefę B, choć rano Rex był w A. Współdzielony obiekt zmieniony w jednym miejscu zmienia się wszędzie, gdzie ktoś trzyma do niego odniesienie.
W laboratoriach Parku Jurajskiego obowiązuje surowa zasada - nigdy nie modyfikujesz oryginalnej próbki DNA. Każda zmiana tworzy nową kopię. To samo podejście stosujemy w programowaniu funkcyjnym z danymi - oryginalne struktury pozostają nienaruszone, a każda modyfikacja tworzy nowy obiekt.
Spread operator - kopiowanie i rozszerzanie
Operator spread (...) to podstawowe narzędzie do tworzenia niemutowalnych kopii obiektów i tablic. Zacznijmy od przeniesienia dinozaura do innej strefy:
1// Kopiowanie obiektu z modyfikacja
2const dinosaur = { name: 'Rex', health: 100, zone: 'A' };
3const movedDino = { ...dinosaur, zone: 'B' };
4// dinosaur: { name: 'Rex', health: 100, zone: 'A' } - niezmieniony
5// movedDino: { name: 'Rex', health: 100, zone: 'B' } - nowy obiektPola zapisane po spreadzie nadpisują skopiowane, więc zone: 'B' wygrywa z zone: 'A'. movedDino i dinosaur to dwa różne obiekty, a raport trzymający dinosaur wciąż widzi strefę A.
Z tablicami postępujemy tak samo: element dodajemy przez spread, a usuwamy przez filter:
1// Kopiowanie tablicy z dodaniem elementu
2const zones = ['A', 'B', 'C'];
3const extendedZones = [...zones, 'D'];
4// zones: ['A', 'B', 'C'] - niezmieniona
5// extendedZones: ['A', 'B', 'C', 'D']
6
7// Usuwanie elementu z tablicy (bez mutacji)
8const dinosaurs = ['Rex', 'Blue', 'Brachio', 'Stego'];
9const withoutBlue = dinosaurs.filter((d) => d !== 'Blue');
10// dinosaurs: ['Rex', 'Blue', 'Brachio', 'Stego'] - niezmieniona
11// withoutBlue: ['Rex', 'Brachio', 'Stego']Żadna z tych operacji nie użyła push ani splice, które zmieniają tablicę w miejscu.
Zagnieżdżone obiekty - głęboka niemutowalność
Spread operator tworzy płytką kopię - zagnieżdżone obiekty nadal są współdzielone! Musisz spreadować na każdym poziomie. Oto stan parku ze strefami w środku i próba kopii jednym spreadem:
1const park = {
2 name: 'Jurassic Park',
3 zones: {
4 A: { dinosaurs: ['Rex'], voltage: 10000 },
5 B: { dinosaurs: ['Brachio'], voltage: 5000 },
6 },
7};
8
9// ŹLE - płytka kopia, zones jest współdzielone
10const badCopy = { ...park };
11badCopy.zones.A.voltage = 0; // ZMIENIA też oryginał!badCopy to nowy obiekt, ale jego pole zones wskazuje na ten sam obiekt co park.zones. Zmiana napięcia przez kopię jest więc zmianą oryginału, dokładnie jak w historii z mapą i raportem.
Poprawna aktualizacja kopiuje każdy poziom na ścieżce do zmienianego pola. Zakładamy świeży obiekt park, bo linijka z badCopy zdążyła już wyzerować oryginał:
1// DOBRZE - głęboka kopia na każdym poziomie
2const updatedPark = {
3 ...park,
4 zones: {
5 ...park.zones,
6 A: {
7 ...park.zones.A,
8 voltage: 15000,
9 },
10 },
11};
12// park.zones.A.voltage: 10000 (niezmieniony)
13// updatedPark.zones.A.voltage: 15000 (nowy obiekt)Nowe są tylko obiekty na ścieżce: updatedPark, jego zones i strefa A. Strefa B nie została skopiowana, więc updatedPark.zones.B === park.zones.B. To w porządku, bo jej nie zmienialiśmy, a współdzielenie niezmienionych części oszczędza pamięć.
Object.freeze() - zamrażanie obiektów
Object.freeze() zapobiega modyfikacji obiektu w runtime. Próba zmiany jest po cichu ignorowana, a w trybie ścisłym (strict mode) rzuca TypeError. W trybie ścisłym działa każdy moduł ES i każda klasa:
1const CONFIG = Object.freeze({
2 maxDinosaurs: 100,
3 maxVoltage: 50000,
4 alertThreshold: 3,
5});
6
7CONFIG.maxDinosaurs = 200; // Ignorowane! (lub TypeError w strict mode)
8console.log(CONFIG.maxDinosaurs); // nadal 100Object.freeze zwraca ten sam obiekt, tylko zamrożony: nie da się zmienić, dodać ani usunąć jego pól. Jest jednak haczyk, widoczny przy obiekcie z zagnieżdżoną strukturą:
1// UWAGA: freeze jest płytki!
2const deepConfig = Object.freeze({
3 zones: { A: { limit: 10 } },
4});
5deepConfig.zones.A.limit = 999; // TO ZADZIAŁA - freeze jest płytkiZamrożony jest tylko pierwszy poziom. Obiekt zones pozostał zwykłym obiektem, więc żeby zamrozić całość, trzeba rekurencyjnie wywołać Object.freeze na każdym zagnieżdżonym obiekcie.
const to nie freeze
Częste nieporozumienie: skoro CONFIG jest zadeklarowane przez const, po co jeszcze freeze? Bo te dwa mechanizmy chronią zupełnie co innego:
1const zone = { id: 'A', voltage: 10000 };
2zone.voltage = 0; // działa - const nie chroni zawartości
3// zone = { id: 'B' }; // TypeError - const chroni przypisanie
4
5const lockedZone = Object.freeze({ id: 'A', voltage: 10000 });
6lockedZone.voltage = 0; // ignorowane (TypeError w strict mode)const blokuje ponowne przypisanie zmiennej, czyli referencję. Object.freeze blokuje zmianę wartości wewnątrz obiektu. Stałą konfigurację chroń obydwoma naraz, tak jak CONFIG.
structuredClone() - głębokie kopiowanie
structuredClone() tworzy głęboką kopię obiektu - wszystkie zagnieżdżone struktury są niezależne. To wygodne, gdy zmieniasz wiele rzeczy głęboko w strukturze:
1const original = {
2 name: 'Jurassic Park',
3 zones: [
4 { id: 'A', dinosaurs: [{ name: 'Rex', health: 100 }] },
5 { id: 'B', dinosaurs: [{ name: 'Blue', health: 95 }] },
6 ],
7};
8
9const deepCopy = structuredClone(original);
10deepCopy.zones[0].dinosaurs[0].health = 50;
11
12console.log(original.zones[0].dinosaurs[0].health); // 100 (niezmieniony!)
13console.log(deepCopy.zones[0].dinosaurs[0].health); // 50 (niezależna kopia)Zmiana w kopii nie dotarła do oryginału. structuredClone działa w nowoczesnych przeglądarkach i w Node.js od wersji 17, ale nie skopiuje funkcji (rzuci DataCloneError), a instancje klas zamieni w zwykłe obiekty bez metod. Moja rada: na co dzień kopiuj spreadem tylko ścieżkę, którą zmieniasz, a structuredClone zostaw na chwilę, gdy potrzebujesz w pełni niezależnej kopii danych.
Niemutowalne aktualizacje tablic
Pełny zestaw operacji na tablicach bez mutacji, zebrany na jednej liście dinozaurów:
1const dinosaurs = [
2 { id: 1, name: 'Rex', health: 100 },
3 { id: 2, name: 'Blue', health: 95 },
4 { id: 3, name: 'Brachio', health: 88 },
5];
6
7// Dodawanie
8const added = [...dinosaurs, { id: 4, name: 'Stego', health: 100 }];
9
10// Usuwanie
11const removed = dinosaurs.filter((d) => d.id !== 2);
12
13// Aktualizacja
14const updated = dinosaurs.map((d) =>
15 d.id === 1 ? { ...d, health: 80 } : d
16);
17
18// Sortowanie (bez mutacji - tworzymy kopie przed sort)
19const sorted = [...dinosaurs].sort((a, b) => a.health - b.health);Aktualizacja przez map z operatorem warunkowym to wzorzec, który zobaczysz wszędzie: zmieniony element dostaje nowy obiekt, pozostałe przechodzą bez zmian. Tablicę kopiujemy przed sort, bo sort zmienia ją w miejscu. Od ES2023 masz też gotowe odpowiedniki bez mutacji: toSorted, toReversed, toSpliced i with.
Od prostych technik do bibliotek
Uporządkujmy narzędzia od najprostszego: Object.freeze pilnuje jednego poziomu, spread robi płytką kopię ze zmianą, a structuredClone głęboką kopię. Przy dużych strukturach sięga się po biblioteki. Immutable.js dostarcza trwałe struktury danych (persistent data structures): każda "zmiana" zwraca nową wersję, która współdzieli niezmienione fragmenty z poprzednią. Immer pozwala pisać zwykłe przypisania na szkicu, a potem sam tworzy nową, niemutowalną wersję. Ten sam wzorzec czeka na Ciebie w świecie React, gdzie stan aktualizuje się nową kopią, a nie mutacją.
Pamiętaj: oryginalnej próbki DNA nie ruszasz nigdy - każda zmiana to nowa probówka, a spread, structuredClone i Object.freeze pilnują tej zasady na różnych poziomach.
Kod do tej lekcji: index.js
1// Wzorce niemutowalnosci danych
2console.log("=== Park Jurajski - Ochrona Oryginalnych Probek ===\n");
3
4// --- PRZYKLAD 1: Spread operator ---
5console.log("--- Spread: obiekty ---");
6const dino = { name: "Rex", health: 100, zone: "A" };
7const moved = { ...dino, zone: "B" };
8console.log("Oryginal:", dino);
9console.log("Przeniesiony:", moved);
10
11console.log("\n--- Spread: tablice ---");
12const zones = ["A", "B", "C"];
13const extended = [...zones, "D"];
14console.log("Oryginalne:", zones);
15console.log("Rozszerzone:", extended);
16
17// --- PRZYKLAD 2: Zagniezdzone obiekty ---
18console.log("\n--- Zagniezdzone obiekty ---");
19const park = {
20 name: "Jurassic Park",
21 zones: {
22 A: { dinosaurs: ["Rex"], voltage: 10000 },
23 B: { dinosaurs: ["Brachio"], voltage: 5000 },
24 },
25};
26
27const updatedPark = {
28 ...park,
29 zones: {
30 ...park.zones,
31 A: { ...park.zones.A, voltage: 15000 },
32 },
33};
34
35console.log("Oryginal A voltage:", park.zones.A.voltage);
36console.log("Updated A voltage:", updatedPark.zones.A.voltage);
37console.log("Oryginal B voltage:", park.zones.B.voltage, "(niezmieniony)");
38
39// --- PRZYKLAD 3: Object.freeze ---
40console.log("\n--- Object.freeze ---");
41const CONFIG = Object.freeze({
42 maxDinosaurs: 100,
43 maxVoltage: 50000,
44});
45
46CONFIG.maxDinosaurs = 200; // Ignorowane!
47console.log("maxDinosaurs:", CONFIG.maxDinosaurs, "(nadal 100)");
48
49// --- PRZYKLAD 4: structuredClone ---
50console.log("\n--- structuredClone ---");
51const original = {
52 zones: [{ id: "A", dinos: [{ name: "Rex", hp: 100 }] }],
53};
54const deepCopy = structuredClone(original);
55deepCopy.zones[0].dinos[0].hp = 50;
56
57console.log("Oryginal hp:", original.zones[0].dinos[0].hp, "(niezmieniony!)");
58console.log("Kopia hp:", deepCopy.zones[0].dinos[0].hp);
59
60// --- PRZYKLAD 5: Niemutowalne operacje na tablicach ---
61console.log("\n--- Niemutowalne operacje ---");
62const dinosaurs = [
63 { id: 1, name: "Rex", health: 100 },
64 { id: 2, name: "Blue", health: 95 },
65 { id: 3, name: "Brachio", health: 88 },
66];
67
68// Dodawanie
69const added = [...dinosaurs, { id: 4, name: "Stego", health: 100 }];
70console.log("Dodano (nowa tablica):", added.length, "items");
71
72// Usuwanie
73const removed = dinosaurs.filter((d) => d.id !== 2);
74console.log("Usunieto Blue:", removed.map((d) => d.name));
75
76// Aktualizacja
77const updated = dinosaurs.map((d) =>
78 d.id === 1 ? { ...d, health: 80 } : d
79);
80console.log("Updated Rex:", updated[0].health);
81console.log("Oryginal Rex:", dinosaurs[0].health, "(niezmieniony!)");
82
83// Sortowanie (niemutowalne)
84const sorted = [...dinosaurs].sort((a, b) => a.health - b.health);
85console.log("Posortowane:", sorted.map((d) => d.name + ":" + d.health));
86console.log("Oryginal:", dinosaurs.map((d) => d.name + ":" + d.health));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 robi operator spread (...) z zagnieżdżonymi obiektami?
2. Która metoda kopiowania obiektów w JavaScript tworzy głęboką kopię (deep clone)?
Zadania praktyczne w grze
- Edytor kodu
Dodaj, usuń, zaktualizuj dane w zagnieżdżonych obiektach używając spread operator i filter/map.
- Układanie w pionie
Ułóż techniki zapewniania niemutowalności danych od najprostszej do najbardziej zaawansowanej:
- Edytor kodu
Zbuduj pipeline z walidacją, klasyfikacją alertów, grupowaniem i statystykami.
- Klikanie w kolejności
Ułóż elementy zamrażania obiektu konfiguracyjnego:
- Układanie w poziomie
Ułóż elementy niemutowalnej aktualizacji elementu w tablicy:
- Klikanie w kolejności
Ułóż elementy niemutowalnego dodawania elementu do tablicy: