Precyzyjne kopiowanie kodu JavaScript za pomocą funkcjiStructuredClone

Platforma jest teraz wyposażona w funkcjęStructuredClone(), która pozwala na głębsze kopiowanie.

Przez najdłuższy czas trzeba było sięgać do obejścia i bibliotek, aby tworzyć głęboką kopię wartości JavaScript. Platforma jest teraz wyposażona w wbudowaną funkcję structuredClone(), która umożliwia głębsze kopiowanie.

Obsługa przeglądarek

  • 98
  • 98
  • 94
  • 15,4

Źródło

Płytkie kopie

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

Jednym ze sposobów utworzenia płytkiej kopii w JavaScripcie przy użyciu operatora rozkładu obiektów ...:

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

const myShallowCopy = {...myOriginal};

Dodanie lub zmiana usługi bezpośrednio w płytkiej kopii będzie miało wpływ tylko na kopię, a nie na oryginał:

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

Dodanie lub zmiana właściwości głęboko zagnieżdżonej ma jednak wpływ zarówno na kopię, jak i na pierwotną:

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

Wyrażenie {...myOriginal} powtarza się w obrębie (liczbowych) właściwości myOriginal przy użyciu operatora rozproszenia. Używa nazwy i wartości właściwości. Pojedynczo przypisuje je do nowo utworzonego, pustego obiektu. W rezultacie powstały obiekt ma taki sam kształt, ale ma własną kopię listy właściwości i wartości. Wartości także są kopiowane, ale tak zwane wartości podstawowe są obsługiwane przez wartość JavaScript inaczej niż inne. Aby zacytować MDN:

W języku JavaScript element podstawowy (wartość podstawowa, typ danych podstawowych) to dane, które nie są obiektem i nie mają żadnych metod. Jest 7 podstawowych typów danych: ciąg znaków, liczba, bigint, wartość logiczna, nieokreślona, symbol i wartość null.

MDN – podstawowe

Wartości niepodstawowe są traktowane jak references, co oznacza, że kopiowanie wartości polega na skopiowaniu odniesienia do tego samego obiektu bazowego, co skutkuje płytkim zachowaniem kopii.

Głębokie kopie

Przeciwieństwem jest głębsza treść. Algorytm kopiowania głębokiego kopiuje pojedynczo właściwości obiektu, ale wywołuje się rekurencyjnie po znalezieniu odwołania do innego obiektu, co powoduje utworzenie kopii tego obiektu. Może to być bardzo ważne, aby mieć pewność, że dwa fragmenty kodu nie współużytkują obiektu i nieświadomie nie modyfikują swojego stanu.

Kiedyś nie było łatwego ani ładnego sposobu tworzenia szczegółowej kopii wartości w JavaScript. Wiele osób korzystało z bibliotek zewnętrznych, takich jak funkcja cloneDeep() w Lodash. Prawdopodobnie najczęstszym rozwiązaniem tego problemu było włamanie oparte na formacie JSON:

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

W rzeczywistości było to tak popularne obejście, że V8 agresywnie zoptymalizował plik JSON.parse(), a zwłaszcza widoczny powyżej wzorzec, aby maksymalnie skrócić czas działania. Jest szybka, ale ma kilka niedociągnięć i wątków:

  • Rekursywne struktury danych: funkcja JSON.stringify() zgłasza błąd, gdy nadaje mu rekurencyjną strukturę danych. Może się to łatwo stać podczas pracy z połączonymi listami lub drzewami.
  • Typy wbudowane: JSON.stringify() zgłasza wartość, jeśli wartość zawiera inne wbudowane biblioteki JS, takie jak Map, Set, Date, RegExp lub ArrayBuffer.
  • Funkcje: JSON.stringify() dyskretnie odrzuca funkcje.

Klonowanie ustrukturyzowane

Platforma potrzebowała już możliwości tworzenia głębokich kopii wartości JavaScript w kilku miejscach. Przechowywanie wartości JS w IndexedDB wymaga pewnej formy serializacji, aby można ją było przechowywać 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 w tym celu algorytm nazywa się „klonowaniem uporządkowanych” i do niedawna nie był łatwo dostępny dla programistów.

To się teraz zmieniło. Specyfikacja HTML została zmodyfikowana, aby udostępnić funkcję o nazwie structuredClone(), która uruchamia dokładnie ten algorytm, i ułatwia programistom tworzenie precyzyjnych kopii wartości JavaScript.

const myDeepCopy = structuredClone(myOriginal);

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

Funkcje i ograniczenia

Uporządkowane klonowanie eliminuje wiele (choć nie wszystkie) wad metody JSON.stringify(). Klonowanie uporządkowane obsługuje cykliczne struktury danych, wiele wbudowanych typów danych i ogólnie jest wydajniejsze i często szybsze.

Mimo to mają jednak pewne ograniczenia, które mogą Cię zniechęcić:

  • Prototypy: jeśli używasz metody structuredClone() z instancją klasy, jako zwracaną wartość otrzymujesz zwykły obiekt, ponieważ ustrukturyzowane klonowanie powoduje odrzucenie łańcucha prototypu obiektu.
  • Funkcje: jeśli obiekt zawiera funkcje, structuredClone() zgłosi wyjątek DataCloneError.
  • Elementy niemożliwe do klonowania: niektórych wartości nie można klonować w strukturze. Dotyczy to w szczególności węzłów Error i DOM. Sprawi, że structuredClone() uruchomi zgłoszenie.

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

Występy

Nie przeprowadzałem(-am) żadnego nowego porównania mikrotestu, ale na początku 2018 r. robiłem to przed udostępnieniem danych structuredClone(). W tamtym okresie usługa JSON.parse() była najszybszą opcją w przypadku bardzo małych obiektów. Oczekuję, że nie zmieni się to. Metody, które opierały się na ustrukturyzowanym 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 wydajny niż JSON.parse(), zalecamy ustawienie go jako domyślnego podejścia do tworzenia precyzyjnych kopii.

Podsumowanie

Jeśli musisz utworzyć głęboką kopię wartości w języku JS – być może używasz stałych 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 czy biblioteki. W ekosystemie JavaScriptu znajduje się teraz structuredClone(). Hurra!