coding_html_java_hero - synolab

Javascript : Les variables de type objet et le prototype

Par SynoLab le 9 janvier 2018

 Lecture 6 minutes

Il y a certaines choses quand on développe ou qu'on debug du JavaScript qu'on aurait aimé savoir pour éviter de tomber dans des pièges fourbes tels que les problèmes classiques de "var" pour les portées de variables. Cela devient d'autant plus complexe à appréhender lorsque l'on a une architecture plus élaborée avec de l'héritage, et donc l'utilisation de méthodes via prototype. Mon intention est de mettre en lumière certaines pratiques que j'ai pu voir et qui doivent être soit bannies, soit utilisées à bon escient lorsqu'on souhaite manipuler un attribut de type objet dans une instance de classe.

this.someObjet.someEntry  != $this->someObject['someEntry']
Illustration mnémotechnique d'une syntaxe JS et PHP, mais pas toujours vraie selon le cas !

Vous ne pensez pas être concerné vu que vous utilisez le dernier framework tendance ? Détrompez vous, le piège est vite arrivé quand on ne sait pas ce que font certaines de leurs fonctionnalités !

Mise en situation

Mieux qu'une introduction, commençons par voir le résultat d'un code simple d'implémentation d'attribut de type objet dans l'objectif d'une utilisation au travers d'une instance. Dans le cas présent, nos objets "state" et "stateProto" illustrent différents états d'un quelconque connecteur :

Ce qui est mis en avant dans ce bout de code c'est que, quelle que soit la méthode appelée ou l'instance initiatrice, si une propriété d'un objet faisant partie de la définition du prototype est modifiée alors toutes les instances voient leurs valeurs modifiées. Pour d'autres langages, on pourrait dire que cela ressemble au comportement d'une variable statique mais voyons dans le cas du JavaScript ce qui se passe au moment de la compilation de cet exemple.

Le principe de la chaîne de prototypes est d'itérer sur les propriétés de l'objet jusqu'à trouver une valeur. Nous pouvons donc découper notre exemple par étapes :

  • L'instance a-t-elle la propriété "state" ? (en code : connector1.hasOwnProperty('state'); // true )
  • L'instance possède la propriété "state" donc c'est la valeur de l'instance qui est retournée
  • L'instance a-t-elle la propriété "stateProto" ? (en code : connector1.hasOwnProperty('stateProto'); // false )
  • L'instance ne possède pas la propriété "stateProto", le prototype de l'objet a-t-il la propriété "stateProto" ? (en code : connector1.__proto__.hasOwnProperty('stateProto'); // true )
  • Le prototype possède la propriété "stateProto" donc c'est la valeur du prototype qui est retourné

 

Pour résumer, à chaque fois que nous allons utiliser la méthode init()  pour tenter d'accéder ou de modifier une valeur au travers de la syntaxe this.stateProto.init , il faut avoir en tête que c'est l'équivalent de la syntaxe this.__proto__.stateProto.init  qui est exécuté et qui fait référence à un seul et même objet, que this  porte sur le contexte de "connector1" ou de "connector2" le compilateur ne fait aucune différence.

L'utilisation de this.__proto__  est uniquement pour l'illustration de ce qu'on voit dans la console, même si cela fonctionne il est préférable de privilégier l'emploi de   Object.getPrototypeOf()  pour des manipulations de ce genre
javascript-prototype-console-exemple
Exemple de chaines de prototypes dans une console

 

Sur la base de ce constat, allons voir ce même type d'implémentation utilisé au travers d'un framework et comprendre comment il est rapidement possible d'oublier cette règle fondamentale.

Déclaration d'un "composant" via un framework

La plupart des frameworks JavaScript mettent à disposition des objets très élaborés exploités au travers d'architectures complexes nous faisant oublier quelques fois les bases de l'héritage en programmation orientée prototype. Afin de se faire une représentation de ces mécanismes avec du code JavaScript "basique" et supporté au sens large, nous allons exclure volontairement l'utilisation Object.create()  et autres outils apportés par ECMAScript 2015+.

Tout commence généralement avec une méthode "extend" permettant la fusion d'objets qui, pour une partie, est généralement implémenté avec la logique suivante :

Pour créer nos "composants" (qui peuvent prendre en fonction des cas la signification d'un contrôleur, d'un modèle, d'un widget, etc.), nous créons des prototypes qui héritent d'autres prototypes à partir d'objets qui vont porter chacun leur logique propre. Notre "framework" va lui être une coquille pour factoriser le principe d'héritage que l'on souhaite avoir pour notre application et qui peut ressembler aux comportements d'autres frameworks, pour une partie encore, par cet exemple de code :

Nous disposons d'un "framework" permettant de faire de l'héritage d'objets, nous pouvons maintenant l'utiliser pour notre logique métier :

Si on se concentre uniquement sur la déclaration de "Person", on pourrait croire que "identityProto" est un attribut de notre objet comme il pourrait l'être pour un autre langage à classes de la POO comme PHP, Java ou autre et être considéré comme une propriété de l'instance. Or comme on a pu le voir précédemment, le comportement n'est pas le même selon la manière d'initialiser notre propriété objet et donc d'accéder aux valeurs. Pour un œil non averti, l'erreur d'avoir utilisé "identityProto" au lieu de "identity" a été masquée par l'abstraction proposée par notre "framework", cela pourrait générer ainsi des heures passées à débugger d'autant plus lorsque ce même objet est réutilisé par plusieurs autres composants avec plusieurs instances de lui même !

POC : https://jsfiddle.net/yannberges/erpfeuo6/

Exemples

À titre d'exemples, nous avons là quelques cas d'implémentation d'un héritage où on devrait être mené à se poser la question de l'appartenance de la propriété de type objet au prototype ou à l'instance. Très loin de former une liste exhaustive, cela peut aider à se projeter face à ce type de déclaration de propriétés lorsqu'on utilise des outils et technologies à chaque fois différents :

Conclusion

L'important est que les intentions du développeur soient claires sur l'utilisation qui est faite d'une propriété de type objet dans ce type d'implémentation. Cela peut être pour définir des constantes qui apportent lisibilité et simplicité dans le code ( fileMode === File.modes.EDIT // 'edit'  ), des objets aux ressources partagées pour de l'optimisation mémoire ou de l'interopérabilité de composants, etc ...
Des syntaxes ES6 comme "const" ou "class ... extends ..." peuvent aider pour améliorer la lisibilité de certaines de ces intentions mais elles n'excluent pas totalement la possibilité de tomber dans le type de confusion mis en évidence précédemment entre une valeur récupérée d'une propriété de l'instance et celle du prototype.

GIF