Precyzyjne kopiowanie kodu JavaScript za pomocą funkcjiStructuredClone

Platforma jest teraz wyposażona w StructuredClone() – wbudowaną funkcję kopiowania głębokiego.

Przez najdłuższy czas utworzenie głębokiej kopii wartości JavaScript wymagało odwoływania się do obejścia i bibliotek. Platforma jest teraz wyposażona w wbudowaną funkcję kopiowania głębokiego structuredClone().

Obsługa przeglądarek

  • Chrome: 98.
  • Krawędź: 98.
  • Firefox: 94.
  • Safari: 15.4

Źródło

Płytkie kopie

Kopiowanie wartości w JavaScript prawie zawsze przebiega płytkiej, a nie głębokie. Oznacza to, że zmiany w wartościach głęboko zagnieżdżonych będą widoczne zarówno w kopii, jak i w oryginale.

Jednym ze sposobów utworzenia płytkiej kopii w języku JavaScript jest użycie operatora rozkładu obiektu ...:

const myOriginal = {
  someProp: "with a string value",
  anotherProp: {
    withAnotherProp: 1,
    andAnotherProp: true
  }
};

const myShallowCopy = {...myOriginal};

Dodanie lub zmiana właściwości bezpośrednio w płytkiej kopii ma wpływ tylko na kopię, a nie na oryginał:

myShallowCopy.aNewProp = "a new value";
console.log(myOriginal.aNewProp)
// ^ logs `undefined`

Jednak dodanie lub zmiana właściwości głęboko zagnieżdżonej ma wpływ zarówno na kopię, jak i na oryginał:

myShallowCopy.anotherProp.aNewProp = "a new value";
console.log(myOriginal.anotherProp.aNewProp) 
// ^ logs `a new value`

Wyrażenie {...myOriginal} wykona iterację po właściwościach (wyliczanych) myOriginal przy użyciu operatora rozproszenia. Na podstawie nazwy i wartości właściwości przypisuje je pojedynczo do nowo utworzonego, pustego obiektu. Wynikowy obiekt ma taki sam kształt, ale ma własną kopię listy właściwości i wartości. Wartości również są kopiowane, ale wartości podstawowe są obsługiwane inaczej przez wartości JavaScript niż wartości inne. Aby zacytować MDN:

W języku JavaScript obiekt podstawowy (wartość podstawowa, typ danych) to dane, które nie są obiektami i nie mają metod. Istnieje 7 podstawowych typów danych: string, number, bigint, boolean, undefined, symbol i null.

MDN – podstawowy

Wartości inne niż podstawowe są traktowane jako odwołania, co oznacza, że skopiowanie wartości to tak naprawdę skopiowanie odwołania do tego samego obiektu bazowego, co skutkuje płytkim działaniem kopiowania.

Głębokie kopie

Przeciwieństwem jest płytka treść. Algorytm głębokiego kopiowania również kopiuje po kolei właściwości obiektu, ale wywołuje się rekurencyjnie, gdy znajdzie odwołanie do innego obiektu, tworząc także kopię tego obiektu. Może to być bardzo ważne, aby upewnić się, że dwa fragmenty kodu nie będą miały wspólnego obiektu i nie nieświadomie nie będą manipulowały swoim stanem.

Kiedyś nie było łatwego ani przyjemnego sposobu na utworzenie głębokiej kopii wartości w języku JavaScript. Wiele osób korzystało z bibliotek innych firm, takich jak funkcja cloneDeep() Lodasha. Najczęstszym rozwiązaniem tego problemu było hakowanie w formacie JSON:

const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));

W rzeczywistości było to tak popularne obejście, że V8 intensywnie zoptymalizowało JSON.parse(), a konkretnie powyższy wzorzec, aby zapewnić jak najszybsze działanie. Jest szybki, ma jednak kilka wad i usterek:

  • Rekurencyjne struktury danych: metoda JSON.stringify() odrzuci prośbę, jeśli nadasz jej rekurencyjną strukturę danych. Może się to łatwo zdarzyć podczas pracy z powiązanymi listami lub drzewami.
  • Wbudowane typy: JSON.stringify() spowoduje zgłoszenie, jeśli wartość zawiera inne wbudowane dodatki JS, np. Map, Set, Date, RegExp lub ArrayBuffer.
  • Funkcje: JSON.stringify() dyskretnie odrzuci funkcje.

Klonowanie strukturalne

Platforma potrzebowała już możliwości tworzenia głębokich kopii wartości JavaScript w kilku miejscach: przechowywanie wartości JS w IndexedDB wymaga jakiejś formy serializacji, by można ją było zapisać na dysku, a później poddać deserializacji w celu przywrócenia wartości JS. Podobnie wysyłanie wiadomości do WebWorker przez postMessage() wymaga przeniesienia wartości JS z jednego obszaru JS do innego. Używany do tego algorytm nazywa się „ustrukturyzowany klon”, a do niedawna nie był łatwo dostępny dla deweloperów.

Teraz to się zmieniło. Specyfikacja HTML została zmieniona, aby udostępnić funkcję o nazwie structuredClone(), która działa dokładnie z tym algorytmem, aby umożliwić programistom łatwe tworzenie głębokich kopii wartości JavaScript.

const myDeepCopy = structuredClone(myOriginal);

To wszystko. To cały interfejs API. Więcej szczegółów znajdziesz w artykule MDN.

Funkcje i ograniczenia

Klonowanie strukturalne eliminuje wiele (ale nie wszystkie) wady metody JSON.stringify(). Klonowanie strukturalne może obsługiwać cykliczne struktury danych oraz obsługuje wiele wbudowanych typów danych. Jest też ogólnie bardziej niezawodne i często szybsze.

Nadal jednak występują w nim ograniczenia, które mogą Cię nie interesować:

  • Prototypy: jeśli użyjesz metody structuredClone() z wystąpieniem klasy, zwracany będzie zwykły obiekt. ponieważ klonowanie strukturalne odrzuca łańcuch prototypów obiektu.
  • Funkcje: jeśli obiekt zawiera funkcje, structuredClone() zgłosi wyjątek DataCloneError.
  • Wartości nie do sklonowania: niektóre wartości nie podlegają klonowaniu strukturalnemu, w szczególności węzły Error i DOM. it spowoduje rzut structuredClone().

Jeśli któreś z tych ograniczeń jest przeszkodą w Twoim przypadku użycia, biblioteki takie jak Lodash nadal udostępniają niestandardowe implementacje innych algorytmów do precyzyjnego klonowania, które mogą (ale nie muszą) pasować do Twojego zastosowania.

Wyniki

Nie przeprowadziłem żadnego nowego porównania w ramach mikrotestów porównawczych, ale na początku 2018 roku przeprowadziłem(-am) to jednak przed udostępnieniem structuredClone(). W tamtym czasie funkcja JSON.parse() była najszybszą opcją w przypadku bardzo małych obiektów. Zakładam, że to się nie zmieni. Metody wykorzystujące klonowanie strukturalne były (znacznie) szybsze w przypadku większych obiektów. Biorąc pod uwagę, że nowa wersja structuredClone() nie wymaga nadużywania innych interfejsów API i jest bardziej zaawansowana od JSON.parse(), zalecamy ustawienie jej jako domyślnej metody tworzenia głębokich kopii.

Podsumowanie

Jeśli musisz utworzyć głęboką kopię wartości w języku JS – na przykład dlatego, że używasz stałych struktur danych lub chcesz mieć pewność, że funkcja może modyfikować obiekt bez wpływu na oryginał – nie musisz już sięgać po sposoby obejścia tego problemu lub biblioteki. Ekosystem JS ma teraz structuredClone(). Hurra.