Visit Mozilla.org

Le principe de fermeture en JavaScript:La résolution des noms de propriété sur les objets

Un article de MDC.

Sommaire

ECMAScript définit deux catégories d'objets, "Objet Natif" et "Objet Hôte", avec une sous-catégorie d'objets natifs appelée "Objet Intégré" (ECMA 262 3e Edition, Section 4.3). Les objets natifs appartiennent au langage, et les objets hôte sont fournis par l'environnement, comme, par exemple des objets document, des noeuds du DOM ou autres.

Les objets natifs sont des ensembles flexibles et dynamiques de propriétés nommées (certaines implémentations perdent un peu de dynamisme avec la sous-catégorie des objets intégrés, même si cela n'est généralement pas problématique). Les propriétés nommées d'un objet, après leur définition, contiendront une valeur, qui peut être soit une référence vers un autre objet (les fonctions sont également des objets dans ce contexte) soit une valeur primitive: String, Number, Boolean, Null ou Undefined. Le type primitif Undefined est assez étrange, en ce sens qu'il est possible d'assigner une valeur Undefined à une propriété d'un objet, mais que ce faisant la propriété elle-même n'est pas supprimée de l'objet; elle reste une propriété nommée définie, qui porte simplement la valeur Undefined.

Ce qui suit est une description simplifiée de comment les propriétés sont lues et définies sur un objet, avec un balayage rapide sur le fonctionnement interne du processus.

[modifier] Attribution des valeurs

On peut créer des propriétés nommées sur un objet, ou encore définir des valeurs à des propriétés nommées existantes, en assignant une valeur à cette propriété nommée. Par exemple, avec...

 var refObjet = new Object(); // créer un objet javascript générique

...une propriété avec le nom "testNumber" peut être créée ainsi:

 refObjet.testNumber = 5;
 /* ou */
 refObjet["testNumber"] = 5;

L'objet n'avait aucune propriété "testNumber" avant l'assignation, celle-ci a été créée au moment de l'assignation. Dès lors, n'importe quelle nouvelle assignation n'a plus besoin de re-créer la propriété, mais redéfinit simplement sa valeur:

 refObjet.testNumber = 8;
 /* ou */
 refObjet["testNumber"] = 8;

Les objets javascript ont des prototypes qui peuvent eux-mêmes être des objets (voir plus bas), et ce prototype peut posséder des propriétés nommées. Ceci n'a cependant aucun rôle dans l'assignation. Si une valeur est assignée à une propriété nommée et que l'objet ne possède pas de propriété de ce nom, alors une nouvelle propriété est créée sous ce nom, et la valeur lui est assignée. Si la propriété existe déjà, alors sa valeur est redéfinie.

[modifier] Lecture des valeurs

C'est dans la lecture des valeurs des propriétés que les prototypes entrent en jeu. Si le nom d'une propriété d'un objet est référencé dans une expression accédant la propriété en question (on parle d'accesseur de propriété), alors la valeur de ladite propriété est renvoyée.

 /* Assigner la valeur à une propriété nommée. Si l'objet ne possède
    pas la propriété avec le nom correspondant avant l'assignation, alors
    la propriété sera créée après:
 */
 
 refObjet.testNumber = 8;
 /* Lire la valeur de la propriété: */
 var val = refObjet.testNumber;
 /* et - val - contient maintenant la valeur 8 qui vient juste d'être
    assignée à la propriété nommée de l'objet */

Mais tous les objets peuvent avoir des prototypes, et les prototypes, étant eux-mêmes des objets, peuvent à leur tour avoir des prototypes, qui eux-mêmes peuvent avoir des prototypes etc., formant ce qu'on appelle la chaîne des prototypes. La chaîne des prototypes se termine lorsque l'un des objets de la chaîne a un prototype "null". Le prototype par défaut pour le constructeur Object possède un prototype null, donc:

 var refObjet = new Object(); //créer un objet javascript générique

...crée un objet avec le prototype Object.prototype qui possède lui-même un prototype null. Ainsi, la chaîne de prototypes pour refObjet ne contient qu'un seul objet : Object.prototype. Cependant:

 /* Une fonction constructeur pour créer des objets de type MyObject1 */
 function MyObject1(formalParameter){
   /* Donner à l'objet en cours de construction une propriété appelée
      testNumber, et assigner à cette propriété la valeur passée au
      constructeur comme premier argument:
   */
   this.testNumber = formalParameter;
 }
 /* Une fonction constructeur pour créer des objets de type MyObject2 */
 function MyObject1(formalParameter){
   /* Donner à l'objet en cours de construction une propriété appelée
      testString, et assigner à cette propriété la valeur passée au
      constructeur comme premier argument:
   */
   this.testString = formalParameter;
 }
 /* L'opération suivante remplace le prototype par défaut associé à toutes
    les instances de MyObject2, par une instance de MyObject1, passant 
    l'argument 8 au constructeur MyObject1, initialisant ainsi testNumber
    à cette valeur:
 
 MyObject2.prototype = new MyObject1( 8 );
 /* Enfin, on crée une instance de MyObject2 et l'on assigne une référence
    à cet objet à la variable objectRef, passant une chaîne comme premier
    argument du constructeur:
 */
 var objectRef = new MyObject2( "String_Value" );

L'instance de MyObject2 référencée par la variable objectRef possède une chaîne de prototypes. Le premier objet de cette chaîne est l'instance de MyObject1 qui a été créée et assignée à la propriété "prototype" du constructeur MyObject2. L'instance de MyObject1 a un prototype, celui défini par défaut pour les objets génériques javascript, référencé par Object.prototype. Object.prototype possède un prototype null, donc la chaîne s'arrête ici.

Lorsqu'un accesseur de propriété tente de lire une propriété nommée pour l'objet référencé par la variable objectRef, alors toute la chaîne de prototypes se met en route. Dans le cas simple:

 var val = objectRef.testString;

...l'instance de MyObject2 référencée par objectRef possède une propriété avec le nom "testString", et c'est donc la valeur de cette propriété, égale à "String_Value", qui est assignée à la variable val. Toutefois:

 var val = objectRef.testNumber;

...ne peut pas lire de propriété nommée sur l'instance de MyObject2 car elle ne possède pas de propriété ainsi nommée; la valeur de la variable est pourtant égale à 8 plutôt que Undefined, car l'interpéteur, n'ayant pu trouver la propriété nommée correspondante sur l'objet, a examiné le prototype de l'objet. Ce dernier est une instance de MyObject1 et a été créé avec une propriété testNumber possèdant une valeur assignée à 8, ce qui permet ainsi à l'accesseur de propriété d'être évalué à la valeur 8. Ni MyObject1 ou MyObject2 n'ont défini de méthode toString, mais si une accesseur de propriété tente de lire la valeur de la propriété toString à partir de objectRef:

 var val = objectRef.toString;

...la variable val reçoit une référence à une fonction. Cette fonction est la propriété toString de Object.prototype, et elle est renvoyée car l'examen du prototype de objectRef, lorsque objectRef avoue ne pas posséder de propriété toString, se passe sur un objet, et lorsque ce prototype ne montre pas, à son tour, de propriété toString, son propre prototype est examiné. Ce dernier, c'est Object.prototype, qui lui possède une méthod toString et c'est une référence à cet objet fonction qui est donc renvoyée.

Finalement:

 var val = objectRef.madeUpProperty;

...renvoie Undefined, car le processus d'analyse de la chaîne des prototypes ne trouve pas la propriété avec le nom "madeUpProperty", sur aucun des objets de la chaîne, et atteint finalement le prototype de Object.prototype, c'est-à-dire null, conduisant ainsi au renvoi de Undefined.

La lecture de propriétés nommées retourne la première valeur trouvée, sur l'objet d'abord, puis dans la chaîne de prototypes ensuite. L'assignation d'une valeur à une propriété nommée sur un objet va créer une propriété sur cet objet lui-même si aucune propriété portant ce nom n'existe déjà.

Cela signifie que si une valeur était assignée avec objectRef.testNumber=3, une propriété "testNumber" serait créée sur l'instance de MyObject2, et tout essai ultérieur de lecture de cette valeur retrouverait cette valeur telle que définie sur l'objet. La chaîne de prototype n'a plus besoin d'être examinée pour résoudre l'accession de la propriété, mais l'instance de MyObject1 avec la valeur de 8 assignée à sa propriété "testNumber" reste non modifiée. L'assignation à objectRef masque simplement la propriété correspondante dans la chaîne des prototypes.

Note : ECMAScript définit une propriété interne prototype de type objet interne. Cette propriété n'est pas directement accessible via script, mais c'est la chaîne des objets référencés avec la propriété interne prototype qui est utilisée dans la résolution de l'accesseur: c'est la chaîne des prototypes de l'objet. Une propriété publique "prototype" existe pour permettre l'assignation, la définition et la manipulation des prototypes en relation avec la propriété interne prototype. Les détails de la relation entre ces deux éléments sont décrits dans ECMA 262 (3e Edition) et n'entrent pas dans le périmètre de cet article.

< Introduction | Résolution d'identifiants, contextes d'exécution et visibilité des variables >