IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Voir le flux RSS

Gugelhupf

[Actualité] JavaScript - Réaliser une copie parfaite d'objet

Noter ce billet
par , 27/12/2015 à 02h35 (2076 Affichages)
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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

Envoyer le billet « JavaScript - Réaliser une copie parfaite d'objet » dans le blog Viadeo Envoyer le billet « JavaScript - Réaliser une copie parfaite d'objet » dans le blog Twitter Envoyer le billet « JavaScript - Réaliser une copie parfaite d'objet » dans le blog Google Envoyer le billet « JavaScript - Réaliser une copie parfaite d'objet » dans le blog Facebook Envoyer le billet « JavaScript - Réaliser une copie parfaite d'objet » dans le blog Digg Envoyer le billet « JavaScript - Réaliser une copie parfaite d'objet » dans le blog Delicious Envoyer le billet « JavaScript - Réaliser une copie parfaite d'objet » dans le blog MySpace Envoyer le billet « JavaScript - Réaliser une copie parfaite d'objet » dans le blog Yahoo

Mis à jour 31/12/2015 à 11h25 par Gugelhupf (Remise au centre des tableaux, car la rectification précédente n'a pas eu l'effet voulu)

Catégories
Javascript , Développement Web , Programmation

Commentaires

  1. Avatar de progdebutant
    • |
    • permalink
    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.
    Mis à jour 30/12/2015 à 12h04 par progdebutant (faute)
  2. Avatar de autran
    • |
    • permalink
    Belle maitrise du langage !!!
    Tu ne souhaites toujours pas passer sur Node.js
  3. Avatar de Gnuum
    • |
    • permalink
    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 - Visualiser dans une fenêtre à part
    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 - Visualiser dans une fenêtre à part
    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 - Visualiser dans une fenêtre à part
    1
    2
    // Supprimer:
    deepCopy.__proto__ = Object.create(originalObject.constructor.prototype);

    par:

    Code javascript : Sélectionner tout - Visualiser dans une fenêtre à part
    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 - Visualiser dans une fenêtre à part
    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.
  4. Avatar de Gugelhupf
    • |
    • permalink
    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 - Visualiser dans une fenêtre à part
    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); 
    }
    Mis à jour 31/12/2015 à 12h49 par Gugelhupf
  5. Avatar de Gnuum
    • |
    • permalink
    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.
  6. Avatar de SylvainPV
    • |
    • permalink
    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
  7. Avatar de danielhagnoul
    • |
    • permalink
    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 - Visualiser dans une fenêtre à part
    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 );