161 votes

Comment faire un clone profond en javascript

Comment cloner profondément un objet JavaScript ?

Je sais qu'il existe différentes fonctions basées sur des cadres tels que JSON.parse(JSON.stringify(o)) y $.extend(true, {}, o) mais je ne veux pas utiliser un tel cadre.

Quelle est la manière la plus élégante ou la plus efficace de créer un clone profond ?

Nous nous intéressons à des cas particuliers tels que le clonage de tableaux. Ne pas briser les chaînes de prototypes, gérer l'autoréférence.

Nous ne nous soucions pas de la prise en charge de la copie d'objets DOM et autres car .cloneNode existe pour cette raison.

Comme je veux surtout utiliser des clones profonds dans les node.js l'utilisation des fonctionnalités ES5 du moteur V8 est acceptable.

[Editer]

Avant que quelqu'un ne le suggère, permettez-moi de préciser qu'il y a une nette différence entre créer une copie en héritant prototypiquement de l'objet et clonage il. Le premier fait des dégâts dans la chaîne de prototypes.

[Suite de la rédaction]

Après avoir lu votre réponse, j'ai fait la fâcheuse découverte que le clonage d'objets entiers est un jeu très dangereux et difficile. Prenons par exemple l'objet suivant, basé sur une fermeture

var o = (function() {
     var magic = 42;

     var magicContainer = function() {
          this.get = function() { return magic; };
          this.set = function(i) { magic = i; };
     }

      return new magicContainer;
}());

var n = clone(o); // how to implement clone to support closures

Existe-t-il un moyen d'écrire une fonction de clonage qui clone l'objet, qui a le même état au moment du clonage mais qui ne peut pas modifier l'état de l'objet ? o sans écrire un parseur JS en JS.

Une telle fonction ne devrait plus être nécessaire dans le monde réel. Il s'agit d'un simple intérêt académique.

197voto

G. Ghez Points 445

Une méthode très simple, peut-être trop simple :

var cloned = JSON.parse(JSON.stringify(objectToClone));

80voto

nemisj Points 2966

Cela dépend vraiment de ce que vous souhaitez cloner. S'agit-il d'un véritable objet JSON ou de n'importe quel objet en JavaScript ? Si vous souhaitez cloner n'importe quel objet, vous risquez d'avoir des problèmes. Quels problèmes ? Je vais l'expliquer ci-dessous, mais tout d'abord, un exemple de code qui clone les objets littéraux, toutes les primitives, les tableaux et les nœuds du DOM.

function clone(item) {
    if (!item) { return item; } // null, undefined values check

    var types = [ Number, String, Boolean ], 
        result;

    // normalizing primitives if someone did new String('aaa'), or new Number('444');
    types.forEach(function(type) {
        if (item instanceof type) {
            result = type( item );
        }
    });

    if (typeof result == "undefined") {
        if (Object.prototype.toString.call( item ) === "[object Array]") {
            result = [];
            item.forEach(function(child, index, array) { 
                result[index] = clone( child );
            });
        } else if (typeof item == "object") {
            // testing that this is DOM
            if (item.nodeType && typeof item.cloneNode == "function") {
                result = item.cloneNode( true );    
            } else if (!item.prototype) { // check that this is a literal
                if (item instanceof Date) {
                    result = new Date(item);
                } else {
                    // it is an object literal
                    result = {};
                    for (var i in item) {
                        result[i] = clone( item[i] );
                    }
                }
            } else {
                // depending what you would like here,
                // just keep the reference, or create new object
                if (false && item.constructor) {
                    // would not advice to do that, reason? Read below
                    result = new item.constructor();
                } else {
                    result = item;
                }
            }
        } else {
            result = item;
        }
    }

    return result;
}

var copy = clone({
    one : {
        'one-one' : new String("hello"),
        'one-two' : [
            "one", "two", true, "four"
        ]
    },
    two : document.createElement("div"),
    three : [
        {
            name : "three-one",
            number : new Number("100"),
            obj : new function() {
                this.name = "Object test";
            }   
        }
    ]
})

Parlons maintenant des problèmes que vous pouvez rencontrer lorsque vous commencez à cloner des objets REELS. Je parle ici des objets que vous créez en faisant quelque chose comme

var User = function(){}
var newuser = new User();

Bien sûr, vous pouvez les cloner, ce n'est pas un problème, chaque objet expose une propriété de constructeur, et vous pouvez l'utiliser pour cloner des objets, mais cela ne fonctionnera pas toujours. Vous pouvez aussi faire un simple for in sur ces objets, mais il va dans la même direction - les ennuis. J'ai également inclus la fonctionnalité de clonage dans le code, mais elle est exclue par if( false ) déclaration.

Pourquoi le clonage est-il un problème ? Tout d'abord, chaque objet/instance peut avoir un certain état. Vous ne pouvez jamais être sûr que vos objets n'ont pas, par exemple, des variables privées, et si c'est le cas, en clonant l'objet, vous brisez simplement l'état.

Imaginez qu'il n'y ait pas d'État, c'est parfait. Mais nous avons encore un autre problème. Le clonage via la méthode "constructor" nous donnera un autre obstacle. Il s'agit d'une dépendance d'arguments. Vous ne pouvez jamais être sûr que la personne qui a créé cet objet n'a pas fait une sorte de

new User({
   bike : someBikeInstance
});

Si c'est le cas, vous n'avez pas de chance, someBikeInstance a probablement été créée dans un certain contexte et ce contexte n'est pas connu pour la méthode clone.

Que faire alors ? Vous pouvez encore faire for in et de traiter ces objets comme des objets littéraux normaux, mais peut-être serait-il préférable de ne pas cloner ces objets du tout, et de transmettre simplement la référence de cet objet ?

Une autre solution consiste à établir une convention selon laquelle tous les objets qui doivent être clonés doivent implémenter eux-mêmes cette partie et fournir la méthode API appropriée (comme cloneObject). Quelque chose comme cloneNode fait pour les DOM.

C'est vous qui décidez.

48voto

tfmontague Points 172

Les JSON.parse(JSON.stringify()) pour copier en profondeur des objets Javascript est un hack inefficace, car il a été conçu pour les données JSON. Elle ne prend pas en charge les valeurs de undefined o function () {} et les ignorera tout simplement (ou null ) lors de la transformation (marshalling) de l'objet Javascript en JSON.

Une meilleure solution consiste à utiliser une fonction de copie profonde. La fonction ci-dessous copie les objets en profondeur et ne nécessite pas de bibliothèque tierce (jQuery, LoDash, etc.).

function copy(aObject) {
  // Prevent undefined objects
  // if (!aObject) return aObject;

  let bObject = Array.isArray(aObject) ? [] : {};

  let value;
  for (const key in aObject) {

    // Prevent self-references to parent object
    // if (Object.is(aObject[key], aObject)) continue;

    value = aObject[key];

    bObject[key] = (typeof value === "object") ? copy(value) : value;
  }

  return bObject;
}

Nota: Ce code peut vérifier les autoréférences simples (décommenter la section // Prevent self-references to parent object ), mais vous devez également éviter de créer des objets avec des autoréférences lorsque cela est possible. Voir à ce sujet : https://softwareengineering.stackexchange.com/questions/11856/whats-wrong-with-circular-references

33voto

trincot Points 10112

Il y a maintenant structuréClone dans l'API Web, qui fonctionne également avec des références circulaires.


Réponse précédente

Voici une fonction ES6 qui fonctionnera également pour les objets avec des références cycliques :

function deepClone(obj, hash = new WeakMap()) {
    if (Object(obj) !== obj) return obj; // primitives
    if (hash.has(obj)) return hash.get(obj); // cyclic reference
    const result = obj instanceof Set ? new Set(obj) // See note about this!
                 : obj instanceof Map ? new Map(Array.from(obj, ([key, val]) => 
                                        [key, deepClone(val, hash)])) 
                 : obj instanceof Date ? new Date(obj)
                 : obj instanceof RegExp ? new RegExp(obj.source, obj.flags)
                 // ... add here any specific treatment for other classes ...
                 // and finally a catch-all:
                 : obj.constructor ? new obj.constructor() 
                 : Object.create(null);
    hash.set(obj, result);
    return Object.assign(result, ...Object.keys(obj).map(
        key => ({ [key]: deepClone(obj[key], hash) }) ));
}

// Sample data
var p = {
  data: 1,
  children: [{
    data: 2,
    parent: null
  }]
};
p.children[0].parent = p;

var q = deepClone(p);

console.log(q.children[0].parent.data); // 1

Remarque sur les ensembles et les cartes

La question de savoir comment traiter les clés des ensembles et des cartes est discutable : ces clés sont souvent des primitives (auquel cas il n'y a pas de débat), mais elles ne sont pas des clés. puede sont également des objets. Dans ce cas, la question est la suivante : ces clés doivent-elles être clonées ?

On pourrait faire valoir qu'il faut procéder ainsi, de sorte que si ces objets sont modifiés dans la copie, les objets de l'original ne sont pas affectés, et vice versa.

D'autre part, on souhaiterait que si un ensemble/une carte has une clé, cela devrait être vrai à la fois dans l'original et dans la copie -- au moins avant qu'une modification ne soit apportée à l'un ou l'autre d'entre eux. Il serait étrange que la copie soit un ensemble/map qui possède des clés qui n'ont jamais existé auparavant (car elles ont été créées au cours du processus de clonage) : ce n'est certainement pas très utile pour un code qui a besoin de savoir si un objet donné est une clé dans cet ensemble/map ou non.

Comme vous le remarquez, je suis plutôt du deuxième avis : les clés des ensembles et des cartes sont les suivantes valeurs (peut-être les références ) qui doivent rester inchangées.

Ces choix se retrouvent souvent avec d'autres objets (éventuellement personnalisés). Il n'y a pas de solution générale, car tout dépend de la manière dont l'objet cloné est censé se comporter dans votre cas particulier.

33voto

razzkumar Points 51

Nous pouvons réaliser un clonage profond en utilisant structuréClone()

const original = { name: "stack overflow" };

// Clone it
const clone = structuredClone(original);

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X