Precyzyjne kopiowanie kodu JavaScript za pomocą funkcjiStructuredClone

Platforma zawiera teraz funkcję structuredClone(), która jest wbudowaną funkcją do głębokiego kopiowania.

Przez długi czas trzeba było uciekać się do obejść i bibliotek, aby utworzyć głęboką kopię wartości JavaScriptu. Platforma zawiera teraz structuredClone(), czyli wbudowaną funkcję głębokiego kopiowania.

Obsługa przeglądarek

  • Chrome: 98.
  • Edge: 98.
  • Firefox: 94.
  • Safari: 15.4.

Źródło

Kopie powierzchowne

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

Jednym ze sposobów utworzenia płytkiej kopii w JavaScriptzie jest użycie operatora rozszerzania 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 wpłynie 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 na oba obiekty:

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

Wyrażenie {...myOriginal} iteruje (wylicza) właściwości myOriginal za pomocą operatora rozprzestrzeniania. Używa nazwy i wartości właściwości, przypisując je po kolei do nowo utworzonego, pustego obiektu. W rezultacie obiekt ma identyczny kształt, ale zawiera własną kopię listy właściwości i wartości. Wartości są też kopiowane, ale tak zwane wartości prymitywne są obsługiwane przez wartość JavaScript inaczej niż wartości nieprzymitywne. Cytat z MDN:

W JavaScriptie typ danych prymitywny (wartość prymitywna, prymitywny typ danych) to dane, które nie są obiektem i nie mają metod. Istnieje 7 pierwotnych typów danych: ciąg znaków, liczba, bigint, boolean, undefined, symbol i null.

MDN – Primitive

Wartości niepierwotne są traktowane jako odwołania, co oznacza, że kopiowanie wartości jest w istocie kopiowaniem odwołania do tego samego obiektu podstawowego, co powoduje płytkie kopiowanie.

Kopie głębokie

Przeciwieństwem płytkiej kopii jest głęboka kopia. Algorytm głębokiego kopiowania również kopiuje właściwości obiektu pojedynczo, ale wywołuje się rekurencyjnie, gdy znajdzie odwołanie do innego obiektu, tworząc też kopię tego obiektu. Może to być bardzo ważne, aby mieć pewność, że 2 fragmenty kodu nie udostępniają przypadkowo obiektu i nie manipulują nieświadomie stanem siebie nawzajem.

Wcześniej nie było łatwego ani przyjemnego sposobu na tworzenie głębokiego kopiowania wartości w JavaScript. Wielu użytkowników korzystało z bibliotek innych firm, takich jak funkcja cloneDeep() w bibliotece Lodash. Najczęstszym rozwiązaniem tego problemu było prawdopodobnie obejście za pomocą pliku JSON:

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

Było to tak popularne rozwiązanie, że V8 agresywnie zoptymalizował JSON.parse(), a w szczególności wzór powyżej, aby był jak najszybszy. Chociaż jest szybki, ma kilka wad i niebezpieczeństw:

  • Rekurencja struktur danych: funkcja JSON.stringify() rzuci wyjątek, jeśli podasz jej rekurencyjną strukturę danych. Może się to zdarzyć podczas pracy z połączonymi listami lub drzewami.
  • Wbudowane typy: JSON.stringify() spowoduje błąd, jeśli wartość zawiera inne wbudowane typy JS, takie jak Map, Set, Date, RegExp lub ArrayBuffer.
  • Funkcje: JSON.stringify() – funkcje zostaną po cichu odrzucone.

Strukturyzowane klonowanie

Platforma potrzebowała już wcześniej możliwości tworzenia głębokich kopii wartości JavaScriptu w kilku miejscach: przechowywanie wartości JS w IndexedDB wymaga pewnej formy serializacji, aby można było zapisać ją na dysku i później zdeserializować, aby przywrócić wartość JS. Podobnie wysyłanie wiadomości do WebWorkera za pomocą postMessage() wymaga przeniesienia wartości JS z jednego obszaru JS do innego. Algorytm, który do tego służy, nazywa się „strukturalny klon” i do niedawna nie był łatwo dostępny dla deweloperów.

To się zmieniło. Specyfikacja HTML została zmieniona, aby udostępnić funkcję o nazwie structuredClone(), która wykonuje dokładnie ten algorytm, aby ułatwić deweloperom tworzenie głębokich kopii wartości JavaScriptu.

const myDeepCopy = structuredClone(myOriginal);

To wszystko. To jest cały interfejs API. Jeśli chcesz dowiedzieć się więcej, przeczytaj artykuł w MDN.

Funkcje i ograniczenia

Strukturalne klonowanie eliminuje wiele (choć nie wszystkie) wad techniki JSON.stringify(). Strukturalne klonowanie może obsługiwać cykliczne struktury danych, obsługuje wiele wbudowanych typów danych i jest ogólnie bardziej niezawodne i często szybsze.

Ma on jednak pewne ograniczenia, które mogą Cię zaskoczyć:

  • Prototypy: jeśli użyjesz structuredClone() z instancją klasy, jako wartość zwracaną otrzymasz zwykły obiekt, ponieważ strukturalne sklonowanie odrzuca łańcuch prototypów obiektu.
  • Funkcje: jeśli obiekt zawiera funkcje, structuredClone() wyrzuci wyjątek DataCloneError.
  • Nieklonowalne: niektóre wartości nie mogą być klonowane, zwłaszcza elementy Error i węzły DOM. Spowoduje to wyrzucenie structuredClone().

Jeśli którekolwiek z tych ograniczeń uniemożliwia Ci wykorzystanie biblioteki w danym przypadku użycia, możesz skorzystać z bibliotek takich jak Lodash, które implementują inne algorytmy głębokiego klonowania, które mogą, ale nie muszą być odpowiednie do Twojego przypadku użycia.

Wyniki

Nie przeprowadziliśmy nowego porównania mikrobenchmarków, ale przeprowadziliśmy je na początku 2018 r., zanim udostępniono structuredClone(). Wtedy JSON.parse() była najszybszą opcją w przypadku bardzo małych obiektów. Zakładam, że tak pozostanie. Techniki oparte na strukturalnym klonowaniu były (znacznie) szybsze w przypadku większych obiektów. Biorąc pod uwagę, że nowy interfejs structuredClone() nie wymaga nadużywania innych interfejsów API i jest bardziej niezawodny niż JSON.parse(), zalecam użycie go domyślnie do tworzenia kopii głębokich.

Podsumowanie

Jeśli musisz utworzyć głęboki skopiowany obiekt w JS (np. dlatego, że używasz niezmiennych struktur danych lub chcesz mieć pewność, że funkcja może manipulować obiektem bez wpływu na oryginał), nie musisz już sięgać po obejścia ani bibliotek. Ekosystem JS ma teraz structuredClone(). Hurra.