Precyzyjne kopiowanie kodu JavaScript za pomocą funkcjiStructuredClone

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

Przez najdłuższy czas utworzenie głębokiej kopii wartości JavaScript wymagało odwoływania się do obejścia i bibliotek. 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 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 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 obie instancje:

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. Używa nazwy i wartości właściwości, przypisując je po kolei do utworzonego właśnie pustego obiektu. W efekcie obiekt wynikowy 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 – element podstawowy

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 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 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 hakowanie w formacie 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 łatwo zdarzyć podczas pracy z powiązanymi 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 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) wady techniki JSON.stringify(). Klonowanie strukturalne może obsługiwać cykliczne struktury danych oraz obsługiwać wiele wbudowanych typów danych. Jest też 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óreś z tych ograniczeń jest przeszkodą w Twoim przypadku użycia, biblioteki takie jak Lodash nadal oferują niestandardowe implementacje innych algorytmów do precyzyjnego klonowania, które mogą (ale nie muszą) pasować do Twojego zastosowania.

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 jego oryginał), nie musisz już sięgać po obejścia ani bibliotek. Ekosystem JS ma teraz structuredClone(). Hurra.