Co const chroni w JavaScript
Często mówi się, że const oznacza „niezmienne". Realnie chroni tylko przypisanie nazwy, nie samą wartość - dlatego mutacja tablicy z const działa, a próba podstawienia jej czegoś nowego rzuca błąd.
Często słyszane skojarzenie: const znaczy „niezmienne”. W praktyce słowo kluczowe robi coś innego, a różnica wychodzi już przy pierwszej tablicy:
const teachers = ["Jan", "Nowak"];
teachers[1] = "Adam"; // OK - mutacja zawartości
console.log(teachers); // ["Jan", "Adam"]
teachers = ["Inni"]; // TypeError - re-bindingMutacja działa, re-binding nie. To nie bug ani edge case - tak const jest zaprojektowane.
#Binding vs wartość
const mówi: „ta nazwa będzie wskazywać na tę samą rzecz przez cały blok”. To powiązanie identyfikatora z wartością nazywa się bindingiem - i właśnie binding const blokuje przed zmianą.
Nie blokuje natomiast zmiany zawartości tej rzeczy. Tablica, obiekt, instancja klasy - cokolwiek mutowalnego - można zmieniać dowolnie, byle identyfikator nadal wskazywał na ten sam obiekt w pamięci.
Dla typów prymitywnych (number, string, boolean, null, undefined, symbol, bigint) ta różnica zanika. Liczby albo stringa nie zmodyfikujesz - jedyne, co możesz zrobić, to przypisać do nazwy nową wartość. A na to const nie pozwala.
#Po co tak to zaprojektowano
Dwa powody z czasów ES2015:
- Tania kontrola. Sprawdzenie „czy nazwa wskazuje na tę samą rzecz” jest tanie i statycznie weryfikowalne. Sprawdzenie „czy zawartość się nie zmieniła” wymagałoby
Object.freezealbo deep comparison na każdym kroku - to inny rząd kosztów. - Zgodność z najczęstszą intencją.
consttypowo wyłapuje przypadkowe re-przypisania, a nie przypadkowe mutacje - ktoś nadpisuje twoją zmienną w pętli albo gałęziif-a. Rozwiązuje to od razu, mutacji nie próbując regulować.
Jeśli logika wymaga prawdziwej immutability, są na to osobne narzędzia: Object.freeze (zamraża bezpośrednie pola obiektu - zagnieżdżone obiekty można dalej zmieniać), narzędzia do immutability (Immer, Immutable.js), readonly w TypeScript. Każde z nich pilnuje czego innego niż const.
#Słowo na koniec
const nie obiecuje, że wartość się nie zmieni. Obiecuje, że nazwa nadal wskazuje na to samo. To dwie różne rzeczy, i przy pierwszej tablicy z const zaczynają mieć znaczenie. Trzymaj const jako wybór domyślny - chroni bindingi taniej i jaśniej niż let - ale jeśli logika wymaga immutability, sięgnij po narzędzie, które to obiecuje.
(O wyborze między let a var - czyli o scope tej nazwy, nie o samym bindingu - piszę osobno w nocie o let i var.)