Przejdź do treści

Temat 24 · 2 × 45 min

Referencje, kopiowanie i mutowalność

Dwa najczęstsze „niewytłumaczalne” błędy początkujących brzmią: „zmieniłem kopię, a zmienił się oryginał” i „mam const, a tablica się zmienia”. Oba wynikają z jednego faktu: zmienna w JavaScripcie nie jest pudełkiem z wartością w środku. Jest etykietą (karteczką) przyczepioną do wartości. Dla liczb i napisów to bez znaczenia, dla obiektów i tablic – zmienia wszystko. Ta lekcja daje ci model mentalny, który tłumaczy zachowanie =, const, parametrów funkcji, metod tablic i stanu w React.

Dwa rodzaje wartości

Wartości w JavaScripcie dzielą się na dwie grupy, które zachowują się przy przypisaniu zupełnie inaczej:

Typy proste (prymitywy)
numberstringbooleanundefinednullbigintsymbol
kopiowane przez wartość, niezmienne
Obiekty
Object {}Array []FunctionDateMapSetelementy DOM
przekazywane przez referencję, zmienne (mutowalne)
Rys. 1. Typy proste a obiekty.

Prymityw jest niezmienny: 'abc'.toUpperCase() nie zmienia napisu, tylko tworzy nowy. Zmienna z prymitywem trzyma wartość bezpośrednio, więc let y = x tworzy niezależną kopię. Obiekt leży w pamięci osobno, a zmienna trzyma tylko referencję – „strzałkę” do niego. const b = a kopiuje strzałkę, nie obiekt. Od tej chwili a i b to dwie etykiety na tym samym przedmiocie.

Po lewej: zmienne x i y wskazują na osobne wartości 5 i 7. Po prawej: zmienne a i b dwiema strzałkami wskazują na ten sam obiekt { x: 9 }.
Rys. 2. Prymitywy są kopiowane, obiekty współdzielone. Zmienna to etykieta ze strzałką, nie pudełko.
Sprawdź się

Co wypisze ten kod?

const a = { punkty: 10 };
const b = a;
b.punkty = 99;
console.log(a.punkty);
prymityw-vs-obiekt.js
// prymitywy: niezależne kopie
let x = 5;
let y = x;
y = 7;
console.log(x, y);                 // 5 7

let imie = 'Ala';
let imie2 = imie;
imie2 += 'n';
console.log(imie, imie2);          // Ala Alan

// obiekty: jedna rzecz, dwie etykiety
const a = { punkty: 10 };
const b = a;
b.punkty = 99;
console.log(a.punkty, b.punkty);   // 99 99
console.log(a === b);              // true – ta sama referencja

// tablice to też obiekty
const lista = ['mleko'];
const taSama = lista;
taSama.push('chleb');
console.log(lista);                // ['mleko', 'chleb']
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Słowniczek: mutacja (mutation) to zmiana zawartości istniejącego obiektu (obj.x = 1, tab.push(…)). Przypisanie (zmienna = …) to przeniesienie etykiety na inną wartość. Przypisanie nigdy nie zmienia starego obiektu – tylko od niego „odczepia” zmienną.

mutacja-vs-przypisanie.js
let a = { x: 1 };
let b = a;

b = { x: 2 };        // PRZYPISANIE: b dostaje nową strzałkę do nowego obiektu
console.log(a.x);    // 1 – stary obiekt nietknięty, a dalej na niego wskazuje

b.x = 3;             // MUTACJA nowego obiektu
console.log(a.x);    // 1 – a nie ma z nim nic wspólnego
console.log(a === b); // false
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Argumenty funkcji: ta sama zasada

Funkcja dostaje kopię tego, co jest w zmiennej. Dla prymitywu to kopia wartości – funkcja nie może zmienić zmiennej z zewnątrz. Dla obiektu to kopia referencji – funkcja widzi ten sam obiekt i może go zmutować, ale przypisanie do parametru nie zmienia zmiennej na zewnątrz (parametr to tylko kolejna etykieta).

Sprawdź się

Co wypisze ten kod?

function dodaj(tab, n) {
tab.push(n);
n = 100;
}
const liczby = [1];
let wartosc = 2;
dodaj(liczby, wartosc);
console.log(liczby, wartosc);
argumenty.js
function zmien(obiekt, liczba) {
  obiekt.imie = 'ZMIENIONE';   // mutacja – widoczna na zewnątrz
  liczba = 0;                  // przypisanie – lokalne
  obiekt = { imie: 'NOWY' };   // przypisanie – też lokalne! od tej chwili obiekt to inna etykieta
  obiekt.imie = 'NOWY2';       // mutacja NOWEGO obiektu – oryginał tego nie widzi
}

const uczen = { imie: 'Ala' };
let wiek = 17;
zmien(uczen, wiek);
console.log(uczen.imie, wiek);   // ZMIENIONE 17
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

W praktyce: funkcja, która mutuje argument, jest zaskakująca dla czytelnika (wywołujesz oblicz(koszyk), a koszyk się zmienia). Lepszy zwyczaj: funkcja zwraca nowy obiekt, a oryginał zostawia w spokoju. Tego wymaga React i tak piszą współczesne biblioteki.

const nie znaczy „niezmienny”

const zabrania ponownego przypisania zmiennej: etykieta zostaje na tym samym przedmiocie na zawsze. Nie mówi nic o zawartości tego przedmiotu. const tab = [] z tab.push(1) to poprawny i codzienny kod. Zasada praktyczna z lekcji o zmiennych obowiązuje: domyślnie const, a let tylko, gdy etykieta ma być przenoszona (licznik, akumulator, przełącznik).

Sprawdź się

Która linia rzuci błąd?

const koszyk = ['mysz'];
koszyk.push('kabel');      // 1
koszyk[0] = 'klawiatura';  // 2
koszyk.length = 0;         // 3
koszyk = [];               // 4
const-mutacja.js
const ustawienia = { motyw: 'jasny' };
ustawienia.motyw = 'ciemny';   // OK – mutacja
ustawienia.jezyk = 'pl';       // OK – nowa właściwość
console.log(ustawienia);

const oceny = [4, 5];
oceny.push(3);                 // OK
console.log(oceny);

oceny = [];                    // TypeError: Assignment to constant variable.
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Object.freeze – gdy naprawdę chcesz zablokować zawartość

Object.freeze(obj) zamraża obiekt: nie da się dodać, usunąć ani zmienić jego właściwości. Przydaje się do stałych konfiguracyjnych (cennik, lista statusów). Dwie rzeczy, o których trzeba wiedzieć:

  • W zwykłym skrypcie (tryb nieścisły) próba zmiany zamrożonego obiektu po cichu nic nie robi. W modułach i klasach (tryb ścisły) rzuca TypeError. Sprawdzisz zamrożenie przez Object.isFrozen.
  • freeze jest płytkie: zagnieżdżone obiekty i tablice pozostają zmienne.
freeze.js
const CENNIK = Object.freeze({
  bilet: 26,
  ulgowy: 18,
  dodatki: ['popcorn', 'napój'],
});

CENNIK.bilet = 1;          // w tym trybie: ignorowane bez błędu (w module byłby TypeError)
CENNIK.nowy = 100;         // ignorowane
delete CENNIK.ulgowy;      // ignorowane
console.log(CENNIK.bilet, CENNIK.nowy, CENNIK.ulgowy, Object.isFrozen(CENNIK));

CENNIK.dodatki.push('nachosy'); // zagnieżdżona tablica NIE jest zamrożona
console.log(CENNIK.dodatki);
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Porównywanie obiektów

=== dla obiektów porównuje referencje, nie zawartość: pyta „czy to ten sam przedmiot?”, a nie „czy wyglądają tak samo?”. Dlatego dwa literały o identycznej treści nigdy nie są równe, a tablica jest równa tylko samej sobie. Do porównania zawartości piszesz własną funkcję (lub bibliotekę, np. lodash.isEqual). Sztuczka JSON.stringify(a) === JSON.stringify(b) działa dla prostych danych, ale zależy od kolejności właściwości i gubi undefined.

porownanie.js
console.log({} === {});                 // false – dwa różne obiekty
console.log([1, 2] === [1, 2]);         // false
console.log('ab' === 'ab');             // true – prymitywy porównywane po wartości

const a = { x: 1 };
const b = a;
console.log(a === b);                   // true – ta sama referencja

// porównanie zawartości (płaskie obiekty)
function taSamaZawartosc(o1, o2) {
  const k1 = Object.keys(o1);
  const k2 = Object.keys(o2);
  return k1.length === k2.length && k1.every((k) => o1[k] === o2[k]);
}
console.log(taSamaZawartosc({ x: 1, y: 2 }, { y: 2, x: 1 })); // true
console.log(taSamaZawartosc({ x: 1 }, { x: 1, y: 2 }));       // false

// sztuczka z JSON – uwaga na kolejność kluczy
console.log(JSON.stringify({ x: 1, y: 2 }) === JSON.stringify({ y: 2, x: 1 })); // false!
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Ta sama zasada obowiązuje przy includes, indexOf i Set: [{ id: 1 }].includes({ id: 1 }) to false. Dla obiektów szukaj przez find((o) => o.id === 1).

Metody, które mutują, i te, które nie

Z lekcji o metodach tablic pamiętasz podział. Teraz wiesz, dlaczego jest ważny: metoda mutująca zmienia obiekt widoczny przez wszystkie etykiety, które na niego wskazują – także przez te w innych częściach programu, o których nie myślisz.

Mutują oryginałZwracają nową wartość, oryginał nietknięty
push, pop, shift, unshift, spliceslice, concat, [...tab]
sort, reverse, fill, copyWithintoSorted, toReversed, toSpliced, with
obj.x = …, delete obj.x, Object.assign(obj, …)map, filter, flat, { ...obj }, structuredClone
el.classList.add, el.textContent = … (DOM)wszystkie metody napisów (toUpperCase, slice, replace…)
mutacja-ukryta.js
const wyniki = [12, 45, 7];
const ranking = { oryginal: wyniki, posortowane: wyniki.sort((a, b) => b - a) };

// sort zmutował tablicę, na którą wskazują oba pola – „oryginał” też jest posortowany
console.log(ranking.oryginal, ranking.posortowane, ranking.oryginal === ranking.posortowane);

// poprawnie: toSorted (lub [...wyniki].sort) tworzy nową tablicę
const dane = [12, 45, 7];
const bezMutacji = { oryginal: dane, posortowane: dane.toSorted((a, b) => b - a) };
console.log(bezMutacji.oryginal, bezMutacji.posortowane);
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Kopia płytka

Kopia płytka (shallow copy) tworzy nowy obiekt zewnętrzny i przepisuje do niego właściwości o jeden poziom. Prymitywy są przy tym kopiowane, ale zagnieżdżone obiekty i tablice – tylko jako referencje. Nowy obiekt i stary mają więc wspólne wnętrze.

Dwa osobne obiekty uczen i kopia, każdy z własnym polem imie, ale pole oceny w obu wskazuje strzałką na jedną wspólną tablicę [5, 4].
Rys. 3. Kopia płytka: nowy obiekt zewnętrzny, wspólne obiekty zagnieżdżone.

Sposoby na kopię płytką: spread [...tab] / { ...obj } (najczęstszy), tab.slice(), Array.from(tab), Object.assign({}, obj).

Sprawdź się

Co wypisze ten kod?

const uczen = { imie: 'Ala', oceny: [5, 4] };
const kopia = { ...uczen };
kopia.imie = 'Ola';
kopia.oceny.push(3);
console.log(uczen.imie, uczen.oceny.length);
kopia-plytka.js
const uczen = { imie: 'Ala', oceny: [5, 4] };
const kopia = { ...uczen };

console.log(kopia === uczen);               // false – nowy obiekt zewnętrzny
console.log(kopia.oceny === uczen.oceny);   // true  – wspólna tablica!

kopia.imie = 'Ola';
kopia.oceny.push(3);
console.log(uczen.imie, uczen.oceny);       // Ala [5, 4, 3]

// jak zmienić zagnieżdżenie bez dotykania oryginału? Skopiuj też wnętrze:
const bezpieczna = { ...uczen, oceny: [...uczen.oceny, 6] };
console.log(uczen.oceny, bezpieczna.oceny); // [5, 4, 3] [5, 4, 3, 6]
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Kopia płytka wystarcza, gdy dane są płaskie (same prymitywy) lub gdy zmieniasz tylko pierwszy poziom. To najczęstszy przypadek w codziennym kodzie – wzorzec { ...stan, pole: nowaWartosc }.

Kopia głęboka: structuredClone

Gdy potrzebujesz pełnej, niezależnej kopii z całym zagnieżdżeniem (historia „cofnij”, snapshot formularza, dane do eksperymentów), użyj wbudowanej funkcji structuredClone(wartosc). Jest dostępna we wszystkich przeglądarkach od 2022 r. i w Node od wersji 17. Kopiuje zagnieżdżone obiekty, tablice, Date, Map, Set, a nawet struktury cykliczne.

Ograniczenie: nie kopiuje funkcji – obiekt z metodą rzuci DataCloneError. Elementy DOM też nie przejdą. Dla czystych danych (takich, jakie przechodzą przez JSON) jest idealna.

structuredclone.js
const zamowienie = {
  nr: 1,
  klient: { imie: 'Ewa', adres: { miasto: 'Łódź' } },
  pozycje: [{ nazwa: 'Kabel', ilosc: 2 }],
  data: new Date('2026-10-20'),
};

const kopia = structuredClone(zamowienie);
kopia.klient.adres.miasto = 'Gdańsk';
kopia.pozycje[0].ilosc = 99;
kopia.pozycje.push({ nazwa: 'Mysz', ilosc: 1 });

console.log(zamowienie.klient.adres.miasto, zamowienie.pozycje.length, zamowienie.pozycje[0].ilosc); // Łódź 1 2
console.log(kopia.klient.adres.miasto, kopia.pozycje.length);                                     // Gdańsk 2
console.log(kopia.data.getFullYear(), kopia.data !== zamowienie.data);                             // 2026 true – data skopiowana jako Date, nie napis
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
structuredclone-funkcja.js
const zMetoda = { nazwa: 'x', pokaz() { return this.nazwa; } };
structuredClone(zMetoda); // DataCloneError: pokaz() { … } could not be cloned
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Dlaczego to ważne: stan aplikacji i „cofnij”

Najczęstszy praktyczny skutek: historia zmian. Jeśli do historii wkładasz referencję do aktualnego stanu, a potem ten stan mutujesz, wszystkie „zapisane” wersje zmieniają się razem z nim – historia pokazuje wszędzie to samo. Rozwiązanie: wkładaj kopie albo nigdy nie mutuj – twórz nowy stan przy każdej zmianie. Dokładnie tak działa React: setStan({ ...stan, pole: x }), a zmutowany obiekt o tej samej referencji jest uznawany za „bez zmian” i widok się nie odświeża.

historia-zepsuta.js
// WERSJA ZEPSUTA: historia trzyma referencje do jednej tablicy
let lista = [];
const historia = [];

function dodaj(produkt) {
  historia.push(lista);     // zapisujemy „stan przed” – ale to ta sama tablica…
  lista.push(produkt);      // …którą zaraz mutujemy
}
dodaj('mleko');
dodaj('chleb');
console.log(historia.map((h) => h.join(','))); // obie wersje identyczne – historia bezużyteczna

// WERSJA POPRAWNA: nowy stan zamiast mutacji
let lista2 = [];
const historia2 = [];

function dodaj2(produkt) {
  historia2.push(lista2);          // stara tablica zostaje nietknięta…
  lista2 = [...lista2, produkt];   // …bo tworzymy nową
}
dodaj2('mleko');
dodaj2('chleb');
console.log(historia2.map((h) => h.join(',') || '(pusto)'), lista2);
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Ściąga

ZapisCo robiUwaga
let y = x (prymityw)kopia wartościniezależne
const b = a (obiekt)kopia referencjiten sam obiekt, a === b
b.x = 1mutacja wspólnego obiektuwidoczna przez a
b = { x: 1 }przypisanie – nowa strzałkaa bez zmian
f(obj) + obj.x = 1 w fmutacja widoczna na zewnątrzparametr to kolejna etykieta
f(obj) + obj = {} w flokalne przypisaniena zewnątrz bez zmian
const t = []; t.push(1)OK – const chroni etykietęt = [] → TypeError
Object.freeze(obj)blokuje zmiany pierwszego poziomupłytkie; w trybie nieścisłym cicho ignoruje
Object.isFrozen(obj)czy zamrożonytrue/false
a === b (obiekty)ta sama referencja?{} === {} → false
[...tab], { ...obj }kopia płytkawnętrze wspólne
tab.slice(), Object.assign({}, obj)kopia płytka (starszy zapis)jak wyżej
{ ...obj, pole: nowa }kopia ze zmianą jednego polawzorzec React
structuredClone(x)kopia głębokabez funkcji i elementów DOM
JSON.parse(JSON.stringify(x))kopia głęboka (stara)gubi Date, undefined, NaN
toSorted, with, map, filternowa tablicabezpieczne dla historii
sort, splice, pushmutujązmiana widoczna wszędzie

Zadania

Zadanie 1
łatwe

Przewidź, zanim uruchomisz

Dla każdego z sześciu fragmentów zapisz, co wypisze konsola, i narysuj strzałki (zmienna → wartość) po ostatniej linii. Dopiero potem uruchom rozwiązanie.

JavaScript
// A
let a = 'kot'; let b = a; b = 'pies';            console.log(a);
// B
const o1 = { n: 1 }; const o2 = o1; o2.n = 2;     console.log(o1.n);
// C
const t1 = [1]; const t2 = t1; t2 = [2];          console.log(t1);
// D
const t3 = [1]; let t4 = t3; t4 = [2];            console.log(t3, t4);
// E
const k = { a: 1 }; const m = { ...k }; m.a = 5;  console.log(k.a);
// F
const z = { lista: [1] }; const w = { ...z }; w.lista.push(2); console.log(z.lista);
Pokaż rozwiązanie
przewidz.js
// A: prymitywy – b to niezależna kopia; a zostaje
let a = 'kot'; let b = a; b = 'pies';
console.log('A:', a);            // kot

// B: o2 wskazuje ten sam obiekt – mutacja widoczna przez o1
const o1 = { n: 1 }; const o2 = o1; o2.n = 2;
console.log('B:', o1.n);         // 2

// C: TypeError – t2 jest const, nie można przenieść etykiety (sprawdzamy w try)
try {
  const t1 = [1]; const t2 = t1; t2 = [2];
  console.log('C:', t1);
} catch (e) {
  console.log('C:', e.name);     // TypeError
}

// D: t4 to let – przypisanie przenosi etykietę na nową tablicę; t3 bez zmian
const t3 = [1]; let t4 = t3; t4 = [2];
console.log('D:', t3, t4);       // [1] [2]

// E: spread kopiuje płytko; a to prymityw, więc kopia niezależna
const k = { a: 1 }; const m = { ...k }; m.a = 5;
console.log('E:', k.a);          // 1

// F: spread kopiuje tylko referencję do tablicy lista – jest wspólna
const z = { lista: [1] }; const w = { ...z }; w.lista.push(2);
console.log('F:', z.lista);      // [1, 2]
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Zadanie 2
średnie

Funkcja, która nie psuje danych

Poniższa funkcja ma zwrócić koszyk z cenami po rabacie, ale przy okazji zmienia oryginalny koszyk – po jej wywołaniu koszyk[0].cena nie jest już 100. Przepisz ją tak, żeby oryginał pozostał nietknięty, a wynik był nowym koszykiem. Nie używaj structuredClone – wystarczą map i spread.

JavaScript
const koszyk = [{ nazwa: 'Słuchawki', cena: 100 }, { nazwa: 'Etui', cena: 40 }];

function zRabatem(koszyk, procent) {
  for (const p of koszyk) {
    p.cena = p.cena * (1 - procent / 100);
  }
  return koszyk;
}
Pokaż rozwiązanie
z-rabatem.js
const koszyk = [{ nazwa: 'Słuchawki', cena: 100 }, { nazwa: 'Etui', cena: 40 }];

// map tworzy nową tablicę, spread tworzy nowy obiekt dla każdej pozycji –
// żaden oryginalny obiekt nie jest mutowany
function zRabatem(koszyk, procent) {
  return koszyk.map((p) => ({ ...p, cena: p.cena * (1 - procent / 100) }));
}

const przeceniony = zRabatem(koszyk, 25);
console.log(przeceniony.map((p) => p.cena));   // [75, 30]
console.log(koszyk.map((p) => p.cena));        // [100, 40] – oryginał bez zmian
console.log(koszyk[0] === przeceniony[0]);     // false – osobne obiekty
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Uwaga: sam [...koszyk] by nie wystarczył – skopiowałby tablicę, ale obiekty w niej byłyby wspólne, a p.cena = … dalej mutowałoby oryginały.

Zadanie 3
średnie

Lista zakupów z historią „cofnij”

Edytor listy zakupów ma przyciski „Dodaj”, „Usuń ostatni” i „Cofnij”. Wersja poniżej jest zepsuta: po kilku operacjach „Cofnij” nie przywraca poprzedniego stanu, bo historia trzyma referencje do jednej, mutowanej tablicy. Napraw ją, nie zmieniając HTML. Wskazówka: albo zapisuj kopię stanu do historii, albo przestań mutować i twórz nową tablicę przy każdej zmianie.

JavaScript
let lista = [];
const historia = [];

function zapiszStan() { historia.push(lista); }
function dodaj(nazwa) { zapiszStan(); lista.push(nazwa); renderuj(); }
function usunOstatni() { zapiszStan(); lista.pop(); renderuj(); }
function cofnij() { if (historia.length) { lista = historia.pop(); renderuj(); } }
Pokaż rozwiązanie
Lista zakupów z działającym „Cofnij”
let lista = [];
const historia = [];

function zapiszStan() {
  historia.push(lista); // bezpieczne, bo poniżej NIGDY nie mutujemy tablicy lista
}

function dodaj(nazwa) {
  zapiszStan();
  lista = [...lista, nazwa];   // nowa tablica zamiast push
  renderuj();
}

function usunOstatni() {
  if (lista.length === 0) return;
  zapiszStan();
  lista = lista.slice(0, -1); // nowa tablica zamiast pop
  renderuj();
}

function cofnij() {
  if (historia.length === 0) return;
  lista = historia.pop();     // poprzednia tablica jest nietknięta – można do niej wrócić
  renderuj();
}

function renderuj() {
  document.getElementById('lista').innerHTML = lista.map((p) => `<li>${p}</li>`).join('');
  document.getElementById('info').textContent = `Pozycji: ${lista.length}, kroków do cofnięcia: ${historia.length}`;
}

const pole = document.getElementById('produkt');
document.getElementById('dodaj').addEventListener('click', () => {
  const nazwa = pole.value.trim();
  if (!nazwa) return;
  dodaj(nazwa);
  pole.value = '';
  pole.focus();
});
document.getElementById('usun').addEventListener('click', usunOstatni);
document.getElementById('cofnij').addEventListener('click', cofnij);
renderuj();
Podgląd

Alternatywna naprawa z zachowaniem push/pop: w zapiszStan zapisuj kopię – historia.push([...lista]). Oba podejścia działają; wersja „bez mutacji” jest bliższa temu, jak pisze się stan w React.

Zadanie 4
trudne

Snapshot formularza i porównanie głębokie

Napisz dwie funkcje:

  1. rowne(a, b) – rekurencyjne porównanie zawartości dwóch wartości: prymitywy przez ===, tablice element po elemencie, obiekty klucz po kluczu (niezależnie od kolejności kluczy). rowne({ x: 1, y: [1, { z: 2 }] }, { y: [1, { z: 2 }], x: 1 }) ma dać true.
  2. czyZmieniony(snapshot, aktualny) – zwraca true, jeśli stan formularza różni się od zapisanej kopii. Pokaż na przykładzie, że snapshot zrobiony przez structuredClone wykrywa zmianę w zagnieżdżonym polu, a snapshot zrobiony przez { ...stan } nie wykrywa jej (bo porównuje obiekt z samym sobą).
Pokaż rozwiązanie
rowne.js
function rowne(a, b) {
  if (a === b) return true;                              // ta sama referencja lub równe prymitywy
  if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
  if (Array.isArray(a) !== Array.isArray(b)) return false;

  const klucze = Object.keys(a);
  if (klucze.length !== Object.keys(b).length) return false;
  // każdy klucz z a musi istnieć w b i mieć równą (rekurencyjnie) wartość
  return klucze.every((k) => Object.hasOwn(b, k) && rowne(a[k], b[k]));
}

console.log(rowne({ x: 1, y: [1, { z: 2 }] }, { y: [1, { z: 2 }], x: 1 })); // true
console.log(rowne([1, 2, 3], [1, 2, 4]));                                   // false
console.log(rowne({ a: undefined }, {}));                                   // false – różna liczba kluczy

const czyZmieniony = (snapshot, aktualny) => !rowne(snapshot, aktualny);

// stan formularza z zagnieżdżeniem
const stan = { imie: 'Ala', adres: { miasto: 'Łódź', kod: '90-001' } };

const snapshotGleboki = structuredClone(stan);
const snapshotPlytki = { ...stan };

stan.adres.miasto = 'Gdańsk'; // użytkownik zmienił zagnieżdżone pole

console.log('głęboki wykrywa zmianę:', czyZmieniony(snapshotGleboki, stan)); // true
console.log('płytki wykrywa zmianę:', czyZmieniony(snapshotPlytki, stan));   // false – snapshotPlytki.adres to ten sam obiekt
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Płytki snapshot zawiódł, bo snapshotPlytki.adres === stan.adres – zmiana miasta „zmieniła” też snapshot. Do zapamiętywania stanu z zagnieżdżeniem zawsze używaj kopii głębokiej.

Test powtórkowy

Test powtórkowy

Referencje, kopiowanie i mutowalność: 10 pytań.

0/0poprawnych
Sprawdź się

Co wypisze ten kod?

const a = [1, 2];
const b = a;
b.push(3);
console.log(a.length);
Sprawdź się

Które zdanie o const obj = { x: 1 } jest prawdziwe?

Sprawdź się

Co wypisze ten kod?

function ustaw(o) {
o = { nazwa: 'nowa' };
}
const p = { nazwa: 'stara' };
ustaw(p);
console.log(p.nazwa);
Sprawdź się

Co zwróci [1, 2] === [1, 2]?

Sprawdź się

Co wypisze ten kod?

const org = { dane: { a: 1 } };
const kopia = { ...org };
kopia.dane.a = 2;
console.log(org.dane.a);
Sprawdź się

Która metoda tworzy niezależną kopię zagnieżdżonego obiektu z datami i tablicami?

Sprawdź się

Co wypisze ten kod?

const t = [3, 1, 2];
const u = t.toSorted();
const w = t.sort();
console.log(t === u, t === w);
Sprawdź się

Co wypisze ten kod uruchomiony w zwykłym skrypcie (bez trybu ścisłego)?

const K = Object.freeze({ cena: 10 });
K.cena = 20;
console.log(K.cena);
Sprawdź się

Dlaczego historia w tym kodzie pokaże dwie identyczne wersje?

let stan = [];
const historia = [];
historia.push(stan); stan.push('a');
historia.push(stan); stan.push('b');
Sprawdź się

Który zapis zmienia pole cena w kopii obiektu p, nie dotykając oryginału?