Héritage prototypique de Crockford – Problèmes avec des objets imbriqués

J'ai lu "Javascript: The Good Parts" de Douglas Crockford – et alors qu'il est un peu extrême, je suis à bord de beaucoup de ce qu'il a à dire.

Dans le chapitre 3, il discute des objets et, à un moment donné, définit un modèle (également trouvé ici ) pour simplifier et éviter certaines des difficultés / problèmes qui accompagnent le mot-clé "nouveau" intégré.

if (typeof Object.create !== 'function') { Object.create = function (o) { function F() {} F.prototype = o; return new F(); }; } newObject = Object.create(oldObject); 

J'ai donc essayé de l'utiliser dans un projet sur lequel je travaille, et j'ai remarqué un problème lors de la tentative d'hériter d'objets imbriqués. Si j'écrase une valeur d'un objet imbriqué hérité à l'aide de ce modèle, il écrase l'élément imbriqué tout au long de la chaîne prototype.

L'exemple de Crockford est comme le flatObj dans l'exemple suivant, qui fonctionne bien. Le comportement, cependant, est incompatible avec les objets imbriqués:

 var flatObj = { firstname: "John", lastname: "Doe", age: 23 } var person1 = Object.create(flatObj); var nestObj = { sex: "female", info: { firstname: "Jane", lastname: "Dough", age: 32 } } var person2 = Object.create(nestObj); var nestObj2 = { sex: "male", info: { firstname: "Arnold", lastname: "Schwarzenneger", age: 61 } } var person3 = { sex: "male" } person3.info = Object.create(nestObj2.info); // now change the objects: person1.age = 69; person2.info.age = 96; person3.info.age = 0; // prototypes should not have changed: flatObj.age // 23 nestObj.info.age // 96 ??? nestObj2.info.age // 61 // now delete properties: delete person1.age; delete person2.info.age; delete person3.info.age; // prototypes should not have changed: flatObj.age // 23 nestObj.info.age // undefined ??? nestObj2.info.age // 61 

(Aussi sur un violon )

Est-ce que je fais quelque chose de mal ou est-ce une limitation de ce modèle?

Il n'y a pas d'incohérence. Ne pensez pas aux objets imbriqués: une propriété directe d'un objet est toujours sur son prototype ou sur une propriété propre. Il n'est pas pertinent que la propriété soit une primitive ou un objet.

Alors, quand vous faites

 var parent = { x: {a:0} }; var child = Object.create(parent); 

child.x référencera le même objet que parent.x – cet objet {a:0} . Et lorsque vous modifiez une propriété de celui-ci:

 var prop_val = child.x; // == parent.x prop_val.a = 1; 

Les deux seront affectés. Pour modifier une propriété «imbriquée» indépendamment, vous devez d'abord créer un objet indépendant:

 child.x = {a:0}; child.xa = 1; parent.xa; // still 0 

Ce que vous pouvez faire, c'est

 child.x = Object.create(parent.x); child.xa = 1; delete child.xa; // (child.x).a == 0, because child.x inherits from parent.x delete child.x; // (child).xa == 0, because child inherits from parent 

Ce qui signifie qu'ils ne sont pas absolument indépendants – mais encore deux objets différents.

Je pense que ce qui se passe, c'est que lorsque vous créez la person2 , les propriétés de sex et d' info se réfèrent à celles de nestObj . Lorsque vous faites référence à person2.info , puisque person2 ne redéfinit pas la propriété info , elle passe au prototype et modifie l'objet là-bas.

Il semble que la manière «correcte» de le faire est la façon dont vous construisez la person3 , de sorte que l'objet a son propre objet d' info à modifier et ne passe pas au prototype.

Je lis aussi le livre (lentement), alors je sympathise avec vous. 🙂

J'ai changé d'exemples pour vous donner une meilleure démonstration de ce qui se passe ici. Démonstration

D'abord, nous créons un objet avec trois propriétés; Un nombre, une chaîne et un objet avec une propriété avec une valeur de chaîne.

Ensuite, nous créons un deuxième objet à partir du premier en utilisant Object.create() ;

 var obj1 = { num : 1, str : 'foo', obj : { less: 'more' } }; var obj2 = Object.create( obj1 ); console.log( '[1] obj1:', obj1 ); console.log( '[1] obj2:', obj2 ); 
 "[1] obj1:" [object Object] { num: 1, obj: [object Object] { less: "more" }, str: "foo" } "[1] obj2:" [object Object] { num: 1, obj: [object Object] { less: "more" }, str: "foo" } 

As-tu bien? Nous avons notre premier objet et un deuxième objet copié.

Pas si vite; Voyons ce qui se passe lorsque nous modifions certaines des valeurs sur le premier objet.

 obj1.num = 3; obj1.str = 'bar'; obj1.obj.less = 'less'; console.log( '[2] obj1:', obj1 ); console.log( '[2] obj2:', obj2 ); 
 "[2] obj1:" [object Object] { num: 3, obj: [object Object] { less: "less" }, str: "bar" } "[2] obj2:" [object Object] { num: 3, obj: [object Object] { less: "less" }, str: "bar" } 

Maintenant, nous avons notre premier objet, avec des modifications, et une copie de cet objet. Qu'est-ce qu'il se passe ici?

Vérifions si les objets ont leurs propres propriétés.

 for( var prop in obj1 ) console.log( '[3] obj1.hasOwnProperty( ' + prop + ' ): ' + obj1.hasOwnProperty( prop ) ); for( var prop in obj2 ) console.log( '[3] obj2.hasOwnProperty( ' + prop + ' ): ' + obj2.hasOwnProperty( prop ) ); 
 "[3] obj1.hasOwnProperty( num ): true" "[3] obj1.hasOwnProperty( str ): true" "[3] obj1.hasOwnProperty( obj ): true" "[3] obj2.hasOwnProperty( num ): false" "[3] obj2.hasOwnProperty( str ): false" "[3] obj2.hasOwnProperty( obj ): false" 

obj1 possède toutes ses propriétés, comme nous l'avons défini, mais obj2 ne l'est pas.

Que se passe-t-il lorsque nous modifions certaines des obj2 d' obj2 ?

 obj2.num = 1; obj2.str = 'baz'; obj2.obj.less = 'more'; console.log( '[4] obj1:', obj1 ); console.log( '[4] obj2:', obj2 ); for( var prop in obj1 ) console.log( '[4] obj1.hasOwnProperty( ' + prop + ' ): ' + obj1.hasOwnProperty( prop ) ); for( var prop in obj2 ) console.log( '[4] obj2.hasOwnProperty( ' + prop + ' ): ' + obj2.hasOwnProperty( prop ) ); 
 "[4] obj1:" [object Object] { num: 3, obj: [object Object] { less: "more" }, str: "bar" } "[4] obj2:" [object Object] { num: 1, obj: [object Object] { less: "more" }, str: "baz" } "[4] obj1.hasOwnProperty( num ): true" "[4] obj1.hasOwnProperty( str ): true" "[4] obj1.hasOwnProperty( obj ): true" "[4] obj2.hasOwnProperty( num ): true" "[4] obj2.hasOwnProperty( str ): true" "[4] obj2.hasOwnProperty( obj ): false" 

Donc, num et str changé sur obj2 et pas sur obj1 comme nous le voulions, mais obj1.obj.less changé quand il ne devrait pas avoir.

À partir des hasOwnProperty() , nous pouvons le voir, même si nous avons changé obj2.obj.less , nous n'avons pas défini d' obj2.obj premier. Cela signifie que nous nous référons toujours à obj1.obj.less .

Créez un objet à partir de obj1.obj et attribuez-le à obj2.obj et voyez si cela nous donne ce que nous recherchons.

 obj2.obj = Object.create( obj1.obj ); console.log( '[5] obj1:', obj1 ); console.log( '[5] obj2:', obj2 ); for( var prop in obj1 ) console.log( '[5] obj1.hasOwnProperty( ' + prop + ' ): ' + obj1.hasOwnProperty( prop ) ); for( var prop in obj2 ) console.log( '[5] obj2.hasOwnProperty( ' + prop + ' ): ' + obj2.hasOwnProperty( prop ) ); 
 "[5] obj1:" [object Object] { num: 3, obj: [object Object] { less: "more" }, str: "bar" } "[5] obj2:" [object Object] { num: 1, obj: [object Object] { less: "more" }, str: "baz" } "[5] obj1.hasOwnProperty( num ): true" "[5] obj1.hasOwnProperty( str ): true" "[5] obj1.hasOwnProperty( obj ): true" "[5] obj2.hasOwnProperty( num ): true" "[5] obj2.hasOwnProperty( str ): true" "[5] obj2.hasOwnProperty( obj ): true" 

C'est bon, maintenant obj2 a sa propre propriété obj . Voyons ce qui se passe lorsque nous changeons obj2.obj.less maintenant.

 obj2.obj.less = 'less'; console.log( '[6] obj1:', obj1 ); console.log( '[6] obj2:', obj2 ); 
 "[6] obj1:" [object Object] { num: 3, obj: [object Object] { less: "more" }, str: "bar" } "[6] obj2:" [object Object] { num: 1, obj: [object Object] { less: "less" }, str: "baz" } 

Donc, ce que tout cela nous dit, c'est que, si la propriété n'a pas encore été modifiée sur l'objet créé, toute demande d' get de l'objet créé pour cette propriété sera transmise à l'objet d'origine.

La requête set pour obj2.obj.less = 'more' du bloc de code précédent nécessite d'abord une requête de obj2.obj pour obj2.obj , qui n'existe pas dans obj2 à ce point, donc il avance pour obj1.obj et à son tour obj1.obj.less .

Puis, finalement, lorsque nous lisons obj2 nouveau, nous n'avons pas encore réglé obj2.obj afin que la demande soit envoyée à obj1.obj et renvoie le paramètre que nous avions précédemment changé, ce qui a causé l'effet que la modification d'une propriété de l'objet second objets L'enfant semble changer les deux, mais vraiment, il ne change que le premier.


Vous pouvez utiliser cette fonction pour renvoyer un nouvel objet complètement séparé de l'original de manière récursive.

Démonstration

 var obj1 = { num : 1, str : 'foo', obj : { less: 'more' } }; var obj2 = separateObject( obj1 ); function separateObject( obj1 ) { var obj2 = Object.create( Object.getPrototypeOf( obj1 ) ); for(var prop in obj1) { if( typeof obj1[prop] === "object" ) obj2[prop] = separateObject( obj1[prop] ); else obj2[prop] = obj1[prop]; } return obj2; } console.log( '[1] obj1:', obj1 ); console.log( '[1] obj2:', obj2 ); for( var prop in obj1 ) console.log( '[1] obj1.hasOwnProperty( ' + prop + ' ): ' + obj1.hasOwnProperty( prop ) ); for( var prop in obj2 ) console.log( '[1] obj2.hasOwnProperty( ' + prop + ' ): ' + obj2.hasOwnProperty( prop ) ); 
 "[1] obj1:" [object Object] { num: 1, obj: [object Object] { less: "more" }, str: "foo" } "[1] obj2:" [object Object] { num: 1, obj: [object Object] { less: "more" }, str: "foo" } "[1] obj1.hasOwnProperty( num ): true" "[1] obj1.hasOwnProperty( str ): true" "[1] obj1.hasOwnProperty( obj ): true" "[1] obj2.hasOwnProperty( num ): true" "[1] obj2.hasOwnProperty( str ): true" "[1] obj2.hasOwnProperty( obj ): true" 

Voyons ce qui se passe lorsque nous modifions certaines variables maintenant.

 obj1.num = 3; obj1.str = 'bar'; obj1.obj.less = 'less'; console.log( '[2] obj1:', obj1 ); console.log( '[2] obj2:', obj2 ); 
 "[2] obj1:" [object Object] { num: 3, obj: [object Object] { less: "less" }, str: "bar" } "[2] obj2:" [object Object] { num: 1, obj: [object Object] { less: "more" }, str: "foo" } 

Tout fonctionne exactement comme vous l'avez prévu.