JavaScript - Réaliser une copie parfaite d'objet

Le , par Gugelhupf, Modérateur
Auteur : Gokan EKINCI
Date de première publication : 2015-12-27
Licence : CC BY-NC-SA

Objectif : Réaliser une copie parfaite d’objet

Contraintes :
  • L’objet copié ne devra pas impacter l’objet d’origine si on modifie un attribut (effet indésirable), nous appelerons ce principe le « deep-copy ». On testera le « deep-copy » à partir de l’opérateur « === ».
  • L’objet copié devra pouvoir exécuter les méthodes de l’objet d’origine.
  • L’objet copié devra être du même type que l’objet d’origine.


Nous allons créer un objet de type Foo :
Code JavaScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Foo(levelName, obj){ 
    this.levelName = levelName; 
    this.deep = obj;  
} 
  
Foo.prototype.method1 = function(){ 
    console.log("This method works !"); 
} 
  
var originalObject = new Foo("Level 1",  
  new Foo("Level 2",  
    new Foo("Level 3", null) 
  ) 
); 
  
console.log(originalObject.levelName);            // Level 1 
console.log(originalObject.deep.levelName);       // Level 2 
console.log(originalObject.deep.deep.levelName);  // Level 3

Solutions connues pour copier un objet :
ES6 var copy = Object.assign({}, originalObject);
jQuery (Mod 1) var copy = jQuery.extend({}, originalObject);
jQuery (Mod 2) var copy = jQuery.extend(true, {}, originalObject);
JSON var copy = JSON.parse(JSON.stringify(originalObject));
Ma simple solution (peut ne pas fonctionner avec une version d'Internet Explorer inférieur à 11) :
Code JavaScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function clone(originalObject){ 
    if((typeof originalObject !== 'object') || originalObject === null){ 
        throw new TypeError("originalObject parameter must be an object which is not null"); 
    } 
  
    var deepCopy = JSON.parse(JSON.stringify(originalObject)); 
  
    // Une petite récursivité 
    function deepProto(originalObject, deepCopy){ 
        deepCopy.__proto__ = Object.create(originalObject.constructor.prototype); 
        for(var attribute in originalObject){ 
            if(typeof originalObject[attribute] === 'object' && originalObject[attribute] !== null){ 
                deepProto(originalObject[attribute], deepCopy[attribute]); 
            } 
        } 
    } 
    deepProto(originalObject, deepCopy); 
  
    return deepCopy; 
} 
  
var copy = clone(originalObject);

Deep-copy test :
Code JavaScript : Sélectionner tout
1
2
3
4
5
console.log(copy.levelName); 
console.log(copy.deep.levelName); 
console.log(copy.deep.deep.levelName); 
console.log(originalObject.deep === copy.deep); 
console.log(originalObject.deep.deep === copy.deep.deep);
ES6 output Level 1
Level 2
Level 3
true
true
=> Pas de deep-copy :-(
jQuery (Mod 1) output Level 1
Level 2
Level 3
true
true
=> Pas de deep-copy :-(
jQuery (Mod 2) output Level 1
Level 2
Level 3
true
true
=> Pas de deep-copy :-(
JSON output Level 1
Level 2
Level 3
false
false
=> Deep-copy :-)
Ma simple solution output Level 1
Level 2
Level 3
false
false
=> Deep-copy :-)
Test de type et méthode :
Code JavaScript : Sélectionner tout
1
2
3
4
console.log(copy.constructor.name);      // Type test1 
console.log(copy.deep.constructor.name); // Type test2 
copy.method1();                          // Method test1 
copy.deep.method1();                     // Method test2
ES6 output Object
Object
TypeError: copy.method1 is not a function
=> copy n'est pas de type Foo :-(
=> les méthodes de Foo ne sont pas reconnues :-(
jQuery (Mod 1) output Object
Object
This method works !
This method works !
=> copy n'est pas de type Foo :-(
=> les méthodes de Foo sont reconnues :-)
jQuery (Mod 2) output Object
Object
This method works !
This method works !
=> copy n'est pas de type Foo :-(
=> les méthodes de Foo sont reconnues :-)
JSON output Object
Object
TypeError: copy.method1 is not a function
=> copy n'est pas de type Foo :-(
=> les méthodes de Foo ne sont pas reconnues :-(
Ma simple solution output Foo
Foo
This method works !
This method works !
=> copy est de type Foo :-)
=> les méthodes de Foo sont reconnues :-)
Conclusion des tests :






Deep-copy Méthode reconnue Méthode reconnue (deep level) Type Type (deep level)
ES6 Echec Echec Echec Echec Echec
jQuery (Mod 1) Echec Succès Succès Echec Echec
jQuery (Mod 2) Echec Succès Succès Echec Echec
JSON Succès Echec Echec Echec Echec
Ma simple solution Succès Succès Succès Succès Succès
Vous êtes arrivé à la fin de ce mini-tutoriel pour copier des objets en JavaScript. N'hésitez pas à consulter mon profil et mon site (https://gokan-ekinci.appspot.com/) pour plus d'infos.

N'hésitez pas à donner votre avis


Vous avez aimé cette actualité ? Alors partagez-la avec vos amis en cliquant sur les boutons ci-dessous :


 Poster un commentaire

Avatar de progdebutant progdebutant - Membre habitué https://www.developpez.com
le 30/12/2015 à 12:03
J'ai jamais vu une démonstration aussi précise et bien faite !
Chapeau c'est du travail !

Si on compare les résultats c'est sûr que ta méthode dépasse largement les autres.

Je l'adopterais dés que possible.
Avatar de autran autran - Rédacteur https://www.developpez.com
le 30/12/2015 à 15:34
Belle maitrise du langage !!!
Tu ne souhaites toujours pas passer sur Node.js
Avatar de Gnuum Gnuum - Membre expérimenté https://www.developpez.com
le 31/12/2015 à 11:30
En essayant de modifier au minimum ton code, j'apporterais quand même quelques petites modifications.

Tout d'abord je supprimerais la ligne d'interprétation/parsing de JSON qui ne me semble pas très performante:

Code javascript : Sélectionner tout
1
2
// Supprimer: 
var deepCopy = JSON.parse(JSON.stringify(originalObject));

Ensuite, je refactoriserais rapidement la fonction pour qu'elle est un style plus "fonctionnel" (je rentre des paramètres en entrée et je récupère une sortie), ceci afin de rendre plus simple sa compréhension et de garantir une meilleure testabilité:

Code javascript : Sélectionner tout
1
2
3
4
// Remplacer: 
function deepProto(originalObject, deepCopy){ 
// par: 
function deepProto(originalObject){

Pour finir, je changerais la ligne de remplacement du prototype (et la ligne de parsing JSON supprimée précédemment), qui est connue pour être une pratique assez lente:

Code javascript : Sélectionner tout
1
2
// Supprimer: 
deepCopy.__proto__ = Object.create(originalObject.constructor.prototype);

par:

Code javascript : Sélectionner tout
1
2
3
4
5
6
7
8
9
// Crée un nouvel objet avec le même prototype que l'original 
var deepCopy = Object.create(Object.getPrototypeOf(originalObject)); 
  
// Ajoute les propriétés propres de l'objet original au nouvel objet 
for(var attribute in originalObject){ 
    if(originalObject.hasOwnProperty(attribute)){ 
        deepCopy[attribute] = originalObject[attribute]; 
    } 
}

Ce qui donne, au final:

Code javascript : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function clone(originalObject){  
    if((typeof originalObject !== 'object') || originalObject === null){  
        throw new TypeError("originalObject parameter must be an object which is not null");  
    }  
  
    // Une petite récursivité  
    function deepProto(originalObject){ 
        // Crée un nouvel objet avec le même prototype que l'original 
	var deepCopy = Object.create(Object.getPrototypeOf(originalObject)); 
  
        // Ajoute les propriétés propres de l'objet original au nouvel objet 
        for(var attribute in originalObject){ 
	    if(originalObject.hasOwnProperty(attribute)){ 
                deepCopy[attribute] = originalObject[attribute]; 
            } 
        } 
        // Gère la "deep copy" 
	for(var attribute in originalObject) { 
            if(typeof originalObject[attribute] === 'object' && originalObject[attribute] !== null){  
                deepCopy[attribute] = deepProto(originalObject[attribute]); 
            } 
        } 
  
        return deepCopy; 
    }  
  
    return deepProto(originalObject);  
}  
  
var copy = clone(originalObject);

Je pense que ça rend le code un peu plus lisible et que ça permet d'éviter 2 opérations pas très performantes.
Avatar de Gugelhupf Gugelhupf - Modérateur https://www.developpez.com
le 31/12/2015 à 12:33
Bonjour tout le monde, merci pour vos retours !

@progdebutant, merci progdebutant j'essaye d'être perfectionniste au niveau de ma rédaction. L'objectif était de créer une fonction réalisant une copie "parfaite", donc en respectant les contraintes imposée pour la valeur de retour, mais je ne suis pas sûr que l'implémentation de cette fonctionnalité soit parfaite en soi en ce qui concerne son adoption.

@autran, merci du compliment autran , en ce moment je recherche du travail, donc si une entreprise me propose de faire du Node.js pourquoi pas.

@Gnuum, merci pour ton implémentation de cette fonctionnalité Gnuum, j'étais conscient du niveau de perf de `__proto__` ou la sérialisation JSON, je n'ai pas cherché à créer une implémentation puissante mais une solution simple qui respecte les critères de clonage, partant du principe que chaque interpréteur gère le code JS à sa manière. D'ailleurs suite à ton message j'ai réalisé un benchmark ici : http://jsperf.com/clone-comparison-x . En ce qui me concerne (machine/navigateur), après avoir lancé le run plusieurs fois, j'obtiens des résultats assez aléatoires, les deux implémentations tournent autour de 1.3M d'opérations par seconde à +-11%.

En regardant ton implémentation de plus près, je me suis demandé s'il ne serait pas possible d'avoir une seule boucle au lieu de 2, puis de supprimer la condition hasOwnProperty() sachant que l'objet d'origine aura toujours son attribut :
Code JavaScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function clone(originalObject){  
    if((typeof originalObject !== 'object') || originalObject === null){  
        throw new TypeError("originalObject parameter must be an object which is not null");  
    }  
  
    // Une petite récursivité  
    function deepProto(originalObject){ 
        // Crée un nouvel objet avec le même prototype que l'original 
	var deepCopy = Object.create(Object.getPrototypeOf(originalObject)); 
  
        // Gère la "deep copy" 
	for(var attribute in originalObject) { 
            deepCopy[attribute] = originalObject[attribute]; 
  
            if(typeof originalObject[attribute] === 'object' && originalObject[attribute] !== null){  
                deepCopy[attribute] = deepProto(originalObject[attribute]); 
            } 
        } 
  
        return deepCopy; 
    }  
  
    return deepProto(originalObject);  
}
Avatar de Gnuum Gnuum - Membre expérimenté https://www.developpez.com
le 31/12/2015 à 18:21
Bonne idée le benchmark!
Faire une seule boucle est bien entendu également une bonne idée.
En revanche, je pense que le hasOwnProperty est nécessaire car il permet d'éviter d'écraser les propriétés venant de la chaîne prototypale et ainsi d'optimiser la mémoire tout en optimisant la performance en évitant d'inutiles affectations de nouvelles variables (en gros il évite de copier les propriétés qui ont déjà été copiées car elle appartiennent à un des objets dans la chaîne prototypale qui a été affectée précédemment au nouvel objet lors de sa création avec Object.create()).

J'ai rajouté un 4e cas au benchmark avec hasOwnProperty.
http://jsperf.com/clone-comparison-x/2

Effectivement les optimisations ne donne pas 50% de perf en plus mais c'est le genre de fonction qui peut être appelée en masse donc ça ne coûte rien. D'autant que certains optimiseurs sont peut-être capable de mieux exploiter ça.
Avatar de SylvainPV SylvainPV - Rédacteur/Modérateur https://www.developpez.com
le 02/01/2016 à 13:09
L'astuce du JSON stringify/parse est bien connue pour sa simplicité, mais ajoute les contraintes du JSON, notamment le fait que l'on ne puisse pas gérer les structures circulaires ou que les propriétés d'objets dont la valeur est undefined sont tout simplement supprimées.

Comme alternative à JSON, il y a cet algorithme décrit en HTML5 : https://developer.mozilla.org/en-US/...lone_algorithm

Il est implémenté dans certaines API, comme les History states ou les messages cross-origin avec window.postMessage

Exemples:
- avec History.replaceState (synchrone) : http://jsfiddle.net/jeremy/ghC5U/22/
- avec window.postMessage (asynchrone) : http://jsfiddle.net/jeremy/WWN23/9/

mais a d'autres défauts, comme l'absence de parcours de la chaîne prototypale et le non-support des objets Function.

Au final aucune technique "native" ne fait parfaitement le job, et il vaut mieux écrire ses propres algorithmes de copie. Voilà le meilleur que j'ai pu trouver: https://github.com/WebReflection/cloner
Avatar de danielhagnoul danielhagnoul - Rédacteur https://www.developpez.com
le 07/01/2016 à 23:19
Sous réserve de davantage de tests, il me semble que la méthode deepcopy récursive fonctionne bien.

J'ai fait quelques tests avec prototype et avec class. Ci-dessous le code avec class :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
function clone( originalObject ){  
    if ( ( typeof originalObject !== 'object' ) || originalObject === null ){  
      throw new TypeError( "originalObject parameter must be an object which is not null" );  
    }  
  
    function deepProto( originalObject ){ 
      let deepCopy = Object.create( Object.getPrototypeOf( originalObject ) ); 
       
      for ( let attribute in originalObject ){ 
        deepCopy[ attribute ] = originalObject[ attribute ]; 
         
        if ( typeof originalObject[ attribute ] === 'object' && originalObject[ attribute ] !== null ){  
          deepCopy[ attribute ] = deepProto( originalObject[ attribute ] ); 
        } 
      } 
       
      return deepCopy; 
    } 
     
    return deepProto( originalObject );  
} 
 
const 
  kGetType = function( Obj ){ 
    return Object.prototype.toString.call( Obj ).match( /\s([a-zA-Z]+)/ )[ 1 ].toLowerCase(); 
  }, 
  kPays = Symbol( 'Pays' ), 
  kPaysType = 'string', 
  kSetPays = function( obj, value ){ 
 
    if ( kGetType( value ) === kPaysType ){ 
      obj[ kPays ] = value; 
         
    } else { 
      throw `Type des paramètres incompatibles. 
        pays : ${ kPaysType } attendu`; 
    } 
     
    return obj; 
  }; 
   
class Foo { 
  constructor( levelName, obj, pays ){ 
    this.levelName = levelName; 
    this.deep = obj; 
    this.Obj1 = { 
      "test" : "hello", 
      "Obj2" : { 
        "test" : "bonjour" 
      } 
    } 
    kSetPays( this, pays ); 
  } 
  method1(){ 
    console.log("This method works !"); 
  } 
  get pays( ){ 
    return this[ kPays ]; 
  } 
  set pays( value ){ 
    kSetPays( this, value ); 
  } 
}; 
 
let originalObject = new Foo( 
  "Level 1",  
  new Foo( 
    "Level 2",  
    new Foo( "Level 3", null, "USA" ), 
    "France" 
  ), 
  "Belgique" 
); 
 
let copy = clone( originalObject ); 
 
console.log( originalObject.Obj1 === copy.Obj1 ); 
console.log( originalObject.deep.Obj1 === copy.deep.Obj1 ); 
console.log( originalObject.deep.deep.Obj1 === copy.deep.deep.Obj1 ); 
 
console.log( originalObject.Obj1.Obj2 === copy.Obj1.Obj2 ); 
console.log( originalObject.deep.Obj1.Obj2 === copy.deep.Obj1.Obj2 ); 
console.log( originalObject.deep.deep.Obj1.Obj2 === copy.deep.deep.Obj1.Obj2 ); 
 
copy.deep.deep.pays = "Luxembourg"; 
 
console.log( originalObject.deep.deep.pays , copy.deep.deep.pays );
Responsable bénévole de la rubrique JavaScript : Xavier Lecomte -