Niezmienność jest podstawową zasadą w programowaniu funkcyjnym i ma wiele do zaoferowania również programom obiektowym. Obiekt mutowalny to obiekt, którego stan może być modyfikowany po jego utworzeniu. Obiekt niemutowalny to obiekt, którego stan nie może być modyfikowany po jego utworzeniu.
Jaki jest przykład obiektu niemutowalnego w JavaScript?
W JavaScript niektóre wbudowane typy (liczby, ciągi znaków) są niemutowalne, ale niestandardowe obiekty są zazwyczaj mutowalne.
Niektóre wbudowane, niemutowalne obiekty JavaScript to Math, Date.
Oto kilka sposobów na dodanie/symulację niezmienności na zwykłych obiektach JavaScript.
Stałe właściwości obiektu
Poprzez połączenie writable: false i configurable: false, możesz zasadniczo stworzyć stałą (nie może być zmieniona, ponownie zdefiniowana ani usunięta) jako właściwość obiektu, tak jak:
let myObject = {};
Object.defineProperty(myObject, 'number', {
value: 42,
writable: false,
configurable: false,
});
console.log(myObject.number); // 42
myObject.number = 43;
console.log(myObject.number); // 42
Zapobieganie rozszerzeniom
Jeśli chcesz uniemożliwić dodawanie nowych właściwości do obiektu, ale w pozostałych przypadkach pozostawić resztę właściwości obiektu w spokoju, wywołaj Object.preventExtensions(...):
var myObject = {
a: 2,
};
Object.preventExtensions(myObject);
myObject.b = 3;
myObject.b; // undefined
W trybie niestricte stworzenie b kończy się cicho niepowodzeniem. W trybie ścisłym rzuca TypeError.
Pieczeniowanie (Seal)
Object.seal() tworzy „zapieczętowany” obiekt, co oznacza, że bierze istniejący obiekt i zasadniczo wywołuje na nim Object.preventExtensions(), ale także oznacza wszystkie jego istniejące właściwości jako configurable: false.
Tak więc nie tylko nie możesz dodawać więcej właściwości, ale także nie możesz ponownie konfigurować ani usuwać żadnych istniejących właściwości (chociaż nadal możesz modyfikować ich wartości).
Zamrażanie (Freeze)
Object.freeze() tworzy zamrożony obiekt, co oznacza, że bierze istniejący obiekt i zasadniczo wywołuje na nim Object.seal(), ale także oznacza wszystkie właściwości „dostępu do danych” jako writable:false, tak aby ich wartości nie mogły być zmieniane.
To podejście jest najwyższym poziomem niezmienności, jaki można osiągnąć dla samego obiektu, ponieważ zapobiega wszelkim zmianom w obiekcie lub w jego bezpośrednich właściwościach (chociaż, jak wspomniano powyżej, zawartość wszelkich innych obiektów, do których się odwołuje, pozostaje nienaruszona).
var immutable = Object.freeze({});
Zamrażanie obiektu nie pozwala na dodawanie nowych właściwości do obiektu i zapobiega usuwaniu lub zmienianiu istniejących właściwości. Object.freeze() zachowuje wyliczalność, konfigurowalność, zapisywalność i prototyp obiektu. Zwraca przekazany obiekt i nie tworzy zamrożonej kopii.
Jakie są plusy i minusy niezmienności?
Plusy
- Łatwiejsze wykrywanie zmian - Równość obiektów może być określona w wydajny i łatwy sposób poprzez równość referencyjną. Jest to przydatne do porównywania różnic obiektów w React i Redux.
- Programy z obiektami niezmiennymi są mniej skomplikowane do zrozumienia, ponieważ nie musisz martwić się o to, jak obiekt może ewoluować w czasie.
- Kopie obronne nie są już konieczne, gdy niezmienne obiekty są zwracane z funkcji lub przekazywane do nich, ponieważ nie ma możliwości, aby obiekt niezmienny został przez nie zmodyfikowany.
- Łatwe udostępnianie za pomocą referencji - Jedna kopia obiektu jest tak samo dobra jak inna, więc możesz buforować obiekty lub ponownie używać tego samego obiektu wiele razy.
- Odporne na wątki - Obiekty niezmienne mogą być bezpiecznie używane między wątkami w środowisku wielowątkowym, ponieważ nie ma ryzyka ich modyfikacji w innych jednocześnie działających wątkach.
- Korzystając z bibliotek takich jak ImmutableJS, obiekty są modyfikowane za pomocą współdzielenia strukturalnego, a mniej pamięci jest potrzebne do posiadania wielu obiektów o podobnych strukturach.
Minusy
- Naiwne implementacje niemutowalnych struktur danych i ich operacji mogą skutkować niezwykle słabą wydajnością, ponieważ za każdym razem tworzone są nowe obiekty. Zaleca się używanie bibliotek dla efektywnych niemutowalnych struktur danych i operacji, które wykorzystują współdzielenie strukturalne.
- Alokacja (i dealokacja) wielu małych obiektów zamiast modyfikowania istniejących może powodować wpływ na wydajność. Złożoność alokatora lub modułu zbierającego śmieci zwykle zależy od liczby obiektów na stercie.
- Cykliczne struktury danych, takie jak grafy, są trudne do zbudowania. Jeśli masz dwa obiekty, których nie można modyfikować po inicjalizacji, jak możesz sprawić, by wskazywały na siebie nawzajem?
Jak osiągnąć niezmienność we własnym kodzie?
Alternatywą jest użycie deklaracji const w połączeniu z wyżej wymienionymi technikami tworzenia. Do „mutowania” obiektów używaj operatora spread, Object.assign, Array.concat() itp., aby tworzyć nowe obiekty zamiast modyfikować oryginalny obiekt.
Przykłady:
// Przykład tablicy
const arr = [1, 2, 3];
const newArr = [...arr, 4]; // [1, 2, 3, 4]
// Przykład obiektu
const human = Object.freeze({ race: 'human' });
const john = { ...human, name: 'John' }; // {race: "human", name: "John"}
const alienJohn = { ...john, race: 'alien' }; // {race: "alien", name: "John"}