Śledzisz błąd przez trzy pliki tylko po to, by odkryć, że nieszkodliwa funkcja pomocnicza zmieniła nazwę użytkownika. Nigdy nie dotykałeś oryginału. Przynajmniej tak ci się wydawało. W JavaScript przypisanie jednej zmiennej do drugiej nie zawsze oznacza to, czego się spodziewasz. Język dzieli swoje dane na dwie strategie przechowywania, a zapomnienie o tym, której z nich używasz, sprawia, że ciche mutacje przenikają do kodu produkcyjnego.

Aby temu zapobiec, musisz zrozumieć różnicę między wartościami prymitywnymi a obiektami dokładnie tak, jak robi to silnik.

Prymitywy: Faktyczne kopie

Prymityw to pojedyncza, niepodzielna dana. Nie można go rozłożyć na mniejsze wartości JavaScript. Język definiuje siedem typów prymitywnych: String, Number, Boolean, Undefined, Null, Symbol oraz BigInt.

Ponieważ prymitywy są atomowe, zazwyczaj znajdują się bezpośrednio w wiązaniu zmiennej. Gdy kopiujesz jedną zmienną prymitywną do drugiej, silnik duplikuje rzeczywiste dane. Każda zmienna otrzymuje własne, niezależne miejsce w pamięci.

let a = "Alina";
let b = a;
b = "Ali";

console.log(a); // "Alina"
console.log(b); // "Ali"

Tutaj a pozostaje nienaruszona. Ponowne przypisanie b stworzyło zupełnie nową wartość i skierowało b na nią, podczas gdy a wciąż przechowuje swój oryginalny ciąg znaków. Takie zachowanie to kopiowanie przez wartość (copy by value). Działa ono identycznie dla liczb, wartości logicznych, symboli i reszty rodziny prymitywów. Możesz je przekazywać do funkcji, przypisywać ponownie lub zwracać bez obaw o skutki uboczne dla sąsiednich zmiennych.

Obiekty: Współdzielone adresy

Obiekty są inne. Obiekt to złożony kontener, który grupuje ze sobą wiele elementów danych. Ta kategoria obejmuje zwykłe obiekty, tablice, funkcje, daty i każdy inny typ nieprymitywny. Ponieważ struktury te mogą być duże i zagnieżdżone, JavaScript nie przechowuje całego obiektu wewnątrz zmiennej. Zamiast tego zmienna przechowuje referencję, czyli w zasadzie adres w pamięci wskazujący na rzeczywiste dane przechowywane w innym miejscu.

Gdy przypisujesz obiekt do nowej zmiennej, silnik kopiuje adres, a nie obiekt. Dwie zmienne wskazują teraz na dokładnie ten sam dom.

const user = { name: "Alina" };
const copy = user;
copy.name = "Ali";

console.log(user.name); // "Ali"

Zmiana copy.name zmieniła również user.name, ponieważ obie nazwy odnoszą się do tego samego obiektu bazowego. Jest to kopiowanie przez referencję (copy by reference). To samo zaskoczenie pojawia się w przypadku tablic:

const scores = [82, 91, 74];
const backup = scores;
backup.push(88);

console.log(scores); // [82, 91, 74, 88]

W pamięci znajduje się tylko jedna tablica. scores i backup to jedynie dwa znaki wskazujące na nią.

Słowo kluczowe const dodaje warstwę zamieszania. Deklarowanie obiektu za pomocą const blokuje wiązanie zmiennej, więc nie można jej skierować na nowy adres. Nie blokuje to jednak samego obiektu.

const settings = { theme: "dark" };
settings.theme = "light";        // Works perfectly.
settings = { theme: "dark" };    // TypeError

Programiści często oczekują, że const gwarantuje niezmienność (immutability). Tak nie jest. Zapobiega ono jedynie ponownemu przypisaniu referencji. Jeśli chcesz, aby zawartość była nienaruszalna, musisz skopiować ją z odpowiednią intencją.

Gdzie kryją się prawdziwe błędy

Błędy związane z referencjami rzadko objawiają się jako oczywiste ponowne przypisania zmiennych. Ukrywają się wewnątrz wywołań funkcji.

function addTimestamp(record) {
  record.timestamp = Date.now();
  return record;
}

const original = { id: 1 };
addTimestamp(original);

console.log(original.timestamp); // A number now exists here. Oops.

Parametr record otrzymał kopię referencji. Każda mutacja właściwości wewnątrz funkcji zapisywała dane bezpośrednio w obiekcie wywołującego. Funkcja wyglądała na prostą transformację, a jednak spowodowała wyciek stanu poza granice zakresu (scope).

Ten wzorzec jest szczególnie bolesny w frameworkach UI, takich jak React, gdzie aktualizacje stanu zależą