DeepClone neu denken

Wie Sie in JavaScript wissen, werden Objekte als Referenz kopiert. Aber manchmal müssen Sie ein Objekt tief klonen. Viele js-Bibliotheken bieten für diesen Fall die Implementierung der deepClone-Funktion an. Leider berücksichtigen die meisten Bibliotheken einige wichtige Dinge nicht:

  • Arrays können im Objekt liegen und es ist besser, sie als Arrays zu kopieren
  • Das Objekt kann Felder mit einem Symbol als Schlüssel haben
  • Objektfelder haben andere als die Standarddeskriptoren
  • Funktionen können sich in den Feldern des Objekts befinden und müssen auch geklont werden
  • Ein Objekt hat schließlich einen anderen Prototyp als Object.prototype

Wer es kaputt gemacht hat, habe den ganzen Code unter den Spoiler gelegt
function deepClone(source) { return ({ 'object': cloneObject, 'function': cloneFunction }[typeof source] || clonePrimitive)(source)(); } function cloneObject(source) { return (Array.isArray(source) ? () => source.map(deepClone) : clonePrototype(source, cloneFields(source, simpleFunctor({}))) ); } function cloneFunction(source) { return cloneFields(source, simpleFunctor(function() { return source.apply(this, arguments); })); } function clonePrimitive(source) { return () => source; } function simpleFunctor(value) { return mapper => mapper ? simpleFunctor(mapper(value)) : value; } function makeCloneFieldReducer(source) { return (destinationFunctor, field) => { const descriptor = Object.getOwnPropertyDescriptor(source, field); return destinationFunctor(destination => Object.defineProperty(destination, field, 'value' in descriptor ? { ...descriptor, value: deepClone(descriptor.value) } : descriptor)); }; } function cloneFields(source, destinationFunctor) { return (Object.getOwnPropertyNames(source) .concat(Object.getOwnPropertySymbols(source)) .reduce(makeCloneFieldReducer(source), destinationFunctor) ); } function clonePrototype(source, destinationFunctor) { return destinationFunctor(destination => Object.setPrototypeOf(destination, Object.getPrototypeOf(source))); } 

Meine Implementierung ist in einem funktionalen Stil geschrieben, der mir Zuverlässigkeit, Stabilität und Einfachheit bietet. Da viele leider ihr Denken mit Prozeduralismus und Pseudo-OOP immer noch nicht neu aufbauen können, werde ich jeden Baustein meiner Implementierung erklären:

Die deepClone-Funktion selbst verwendet 1 Argumentquelle - die Quelle, aus der wir klonen werden, und ihr deepClone mit allen oben genannten Funktionen wird zurückgegeben:

 function deepClone(source) { return ({ 'object': cloneObject, 'function': cloneFunction }[typeof source] || clonePrimitive)(source)(); } 

Hier ist alles einfach, abhängig von der Art der Daten in der Quelle wird eine Funktion ausgewählt, die sie klonen kann, und die Quelle selbst wird an sie übertragen.

Sie können auch feststellen, dass das zurückgegebene Ergebnis als Funktion ohne Parameter aufgerufen wird, bevor es an den Benutzer zurückgegeben wird. Dies ist notwendig, da ich den Wert, in den ich klone, im einfachsten Funktor einwickle, um ihn mutieren zu können, ohne die Reinheit der Hilfsfunktionen zu verletzen. Hier ist die Implementierung dieses Funktors:

 function simpleFunctor(value) { return mapper => mapper ? simpleFunctor(mapper(value)) : value; } 

Er kann zwei Dinge tun - Map (wenn die Mapper-Funktion an ihn übergeben wird) und Extrahieren (wenn nichts übergeben wird).

Jetzt analysieren wir die Hilfsfunktionen cloneObject, cloneFunction und clonePrimitive. Jeder von ihnen nimmt 1 Argument der Quelle eines bestimmten Typs und gibt seinen Klon zurück.

Bei der Implementierung von cloneObject sollte berücksichtigt werden, dass Arrays ebenfalls vom Typ Objekt sind. In anderen Fällen müssen sie die Felder und den Prototyp klonen. Hier ist seine Implementierung:

 function cloneObject(source) { return (Array.isArray(source) ? () => source.map(deepClone) : clonePrototype(source, cloneFields(source, simpleFunctor({}))) ); } 

Das Array kann mit der Slice-Methode kopiert werden. Da wir jedoch Deep Cloning haben und das Array nicht nur primitive Werte enthalten kann, wird die Map-Methode mit dem oben beschriebenen DeepClone als Argument verwendet.

Für andere Objekte erstellen wir ein neues Objekt und verpacken es in unseren oben beschriebenen Funktor, klonen die Felder (zusammen mit Deskriptoren) mit der Hilfsfunktion cloneFields und klonen dann den Prototyp mit clonePrototype.

Hilfsfunktionen werde ich unten beschreiben. Betrachten Sie in der Zwischenzeit die Implementierung von cloneFunction :

 function cloneFunction(source) { return cloneFields(source, simpleFunctor(function() { return source.apply(this, arguments); })); } 

Sie können eine Funktion einfach nicht mit der gesamten Logik klonen. Sie können es jedoch in eine andere Funktion einschließen, die das Original mit allen Argumenten und dem Kontext aufruft und das Ergebnis zurückgibt. Solch ein "Klon" wird sicherlich die ursprüngliche Funktion im Speicher behalten, aber es wird ein wenig "wiegen" und die ursprüngliche Logik vollständig reproduzieren. Wir verpacken die geklonte Funktion in einen Funktor und kopieren mit cloneFields alle Felder der ursprünglichen Funktion in diese Funktion, da die Funktion in JS auch ein Objekt ist, das gerade aufgerufen wurde, und daher Felder darin speichern kann.

Möglicherweise hat eine Funktion einen anderen Prototyp als Function.prototype, aber ich habe diesen Extremfall nicht berücksichtigt. Einer der Reize von FP ist, dass wir einfach einen neuen Wrapper über eine vorhandene Funktion hinzufügen können, um die erforderliche Funktionalität zu implementieren.

Der letzte clonePrimitive-Baustein dient zum Klonen primitiver Werte. Da primitive Werte jedoch nach Wert (oder nach Referenz, aber in einigen Implementierungen von JS-Engines unveränderlich) kopiert werden, können wir sie einfach kopieren. Da jedoch nicht erwartet wird, dass wir einen reinen Wert erhalten, sondern einen Wert, der in einen Funktor eingeschlossen ist, den der Extrakt ohne Argumente aufrufen kann, werden wir unseren Wert in eine Funktion einschließen:

 function clonePrimitive(source) { return () => source; } 

Jetzt implementieren wir die oben verwendeten Hilfsfunktionen - clonePrototype und cloneFields

Um einen Prototyp zu klonen, extrahiert clonePrototype einfach den Prototyp aus dem Quellobjekt und setzt ihn durch Ausführen einer Kartenoperation für den resultierenden Funktor auf das Zielobjekt:

 function clonePrototype(source, destinationFunctor) { return destinationFunctor(destination => Object.setPrototypeOf(destination, Object.getPrototypeOf(source))); } 

Das Klonen von Feldern ist etwas komplizierter, daher habe ich die Funktion cloneFields in zwei Teile geteilt . Die externe Funktion übernimmt die Verkettung aller benannten Felder und aller Symbolfelder, empfängt absolut alle Felder und führt sie durch den von der Hilfsfunktion erstellten Reduzierer:

 function cloneFields(source, destinationFunctor) { return (Object.getOwnPropertyNames(source) .concat(Object.getOwnPropertySymbols(source)) .reduce(makeCloneFieldReducer(source), destinationFunctor) ); } 

makeCloneFieldReducer sollte für uns eine Reduzierungsfunktion erstellen, die an die Reduktionsmethode in einem Array aller Felder des Quellobjekts übergeben werden kann. Als Batterie wird unser Funktor verwendet, der das Ziel speichert. Der Reduzierer muss das Handle aus dem Feld des Quellobjekts extrahieren und dem Feld des Zielobjekts zuweisen. Hierbei ist jedoch zu berücksichtigen, dass es zwei Arten von Deskriptoren gibt - mit value und mit get / set. Natürlich muss der Wert geklont werden, aber mit get / set besteht keine solche Notwendigkeit. Ein solcher Deskriptor kann wie folgt zurückgegeben werden:

 function makeCloneFieldReducer(source) { return (destinationFunctor, field) => { const descriptor = Object.getOwnPropertyDescriptor(source, field); return destinationFunctor(destination => Object.defineProperty(destination, field, 'value' in descriptor ? { ...descriptor, value: deepClone(descriptor.value) } : descriptor)); }; } 

Das ist alles. Diese Implementierung von deepClone löst alle Probleme, die am Anfang dieses Artikels auftreten. Darüber hinaus basiert es auf reinen Funktionen und einem Funktor, der alle Garantien bietet, die der Lambda-Rechnung innewohnen.

Ich stelle auch fest, dass ich kein hervorragendes Verhalten für andere Sammlungen als ein Array implementiert habe, das es wert wäre, einzeln geklont zu werden, wie z. B. Map oder Set. In einigen Fällen kann dies jedoch erforderlich sein.

Source: https://habr.com/ru/post/de466535/


All Articles