MDN wants to learn about developers like you: https://qsurvey.mozilla.com/s3/MDN-dev-survey

Les prototypes sont un mécanisme au sein de JavaScript qui permettent aux objets JavaScript d'hériter des propriétés d'autres objets. Les prototypes implémentent un héritage différent de celui rencontré dans les langages de programmation objets habituels. Dans cet article, nous allons aborder ces différences, nous allons aussi voir comment la chaîne de prototypage fonctionne. Nous verrons aussi comment les propriétés prototypes peuvent être utilisées afin d'ajouter des méthodes à des constructeurs existants.

Pré-requis : Une connaissance générale de l'informatique, des notions d'HTML et CSS, une connaissance des bases en JavaScript (voir Premiers pas et Blocs de construction) ainsi que des notions de JavaScript orienté objet (JSOO) (voir Introduction aux objets).
Objectifs : Comprendre le concept de prototype en JavaScript, comprendre comment fonctionne une chaîne de prototypage et comment ajouter de nouvelles méthodes aux propriétés d'un prototype.

Un langage basé sur des prototypes ?

JavaScript est souvent décrit comme un langage basé sur les prototypes, chaque objet ayant un prototype objet dont il hérite des méthodes et des attributs. Un prototype peut lui aussi avoir son prototype objet duquel il héritera des méthodes et des attributs et ainsi de suite. On parle alors de chaîne de prototypage (ou prototype chain en anglais). Cela permet d'expliquer pourquoi différents objets possèdent des attributs et des méthodes à partir d'autres objets.

En réalité, les méthodes et attributs sont définis dans la fonction constructrice de l'objet et non pas dans les instances des objets elles-mêmes.

En programmation orientée objet classique, les classes sont définies, puis lorsque des instances sont créées, l'ensemble des attributs et des méthodes sont copiées dans l'instance. En JavaScript en revanche, tout n'est pas copié, on établit un lien entre l'objet instancié et son constructeur (c'est un lien dans la chaîne de prototypage), on détermine alors les méthodes et les attributs en remontant la chaîne.

Prenons un exemple afin de rendre cela un peu plus clair.

Comprendre les prototypes objet

Reprenons notre exemple dans lequel nous avions écrit notre constructeur Personne(). Chargez cet exemple dans votre navigateur, si vous ne l'avez plus, vous pouvez utiliser notre exemple oojs-class-further-exercises.html example (voir aussi le code source).

Dans cet exemple nous avons défini un constructeur comme suit :

function Personne(prenom, nom, age, genre, interets) {
  
  // property and method definitions
  
};

Nous avons ensuite instancié des objets comme ceci :

var personne1 = new Personne('Bob', 'Smith', 32, 'homme', ['musique', 'ski']);

Si vous entrez  « personne1 » dans votre console JavaScript, vous devriez voir que le navigateur essaie de faire de l'auto-complétion avec les attributs de cet objet.

Dans cette liste vous verrez les membres définis au niveau du prototype objet de personne1 qui n'est autre que le constructeur Personne(). On y trouve les valeurs suivantes : nom, age, genre, interets, bio, et salutation. On peut voir aussi d'autres membres tels que watch, valueOf …  Ces membres particuliers sont définis au niveau du prototype objet du constructeur Personne(), qui est Object. On voit ici une mise en œuvre de la chaine de prototypage.

Que peut-il bien se passer lorsque l'on tente d'appeler une méthode définie pour Object en l'appliquant à Personne1 ? Par exemple :

personne1.valueOf()

Cette méthode renvoie simplement la valeur de l'objet pour lequel elle est appelée. Vous pouvez essayer dans votre console ! Lorsque l'on effectue cet appel, il se produit les choses suivantes :

  • Le navigateur tente de déterminer si l'objet personne1 implémente une méthode valueOf().
  • Aucne n'est présente, le navigateur vérifie donc si le prototype objet de personne1 (Personne) contient cette méthode.
  • Pas de valueOf() non plus, donc le navigateur regarde si le prototype objet du constructeur Personne() (Object) possède cette méthode. Il y en a une, donc il l'appelle et tout va bien !

Note : Encore une fois, il est important d'insister que les méthodes et attributs ne sont pas copiés d'un objet à un autre mais qu'on y accède à chaque fois en remontant la chaine de prototypage.

Note : Il n'existe pas de façon officielle d'accéder directement au prototype d'un objet donné. Les « liens » entre les éléments de la chaine sont définis au sein d'une propriété interne appelée [[prototype]] définie dans la spécification de JavaScript. (voir ECMAScript). Néanmoins la plupart des navigateurs modernes implémentent l'attribut __proto__   (deux tirets soulignés ou underscore de chaque côté) qui contient le prototype objet d'un objet. Vous pouvez tenter personne1.__proto__ et personne1.__proto__.__proto__ pour voir à quoi ressemble une chaine de prototypage dans la console !

L'attribut prototype : Pour définir les membres du prototype qui seront hérités.

Mais du coup, où définissons-nous les attributs et méthodes qui seront hérités au long de la chaine de prototypage ? En effet, s'il on regarde à la page de documentation Object on peut voir un large éventail d'attributs et de méthodes qui sont définis, une liste bien plus longue que celle disponible pour notre objet Personne1. Pourquoi Personne1 hérite de certains de ces éléments mais pas de tous ?

Cela vient du fait que les éléments hérités sont ceux définis au niveau de l'attribut prototype d'Object (on peut voir cet attribut comme un sous espace de noms). Ainsi les éléments listés sont ceux sous Object.prototype. et pas ceux situés juste sous Object. La valeur de l'attribut prototype est un objet qui rassemble les attributs et méthodes que l'on souhaite appliquer aux objets tout au long de la chaine de prototypage.

Ainsi Object.prototype.watch(), Object.prototype.valueOf() …  sont disponibles pour n'importe quel objet qui hérite de Object.prototype ce qui inclus les nouvelles instances créées à partir du constructeur.

Object.is(), Object.keys(), ainsi que d'autres membres non définis dans prototype ne sont pas hérité par les instances ou les objets qui hérite de Object.prototype. Ces méthodes et attributs sont disponibles uniquement pour le constructeur Object().

Note : Ça paraît bizarre, d'avoir une méthode définie au sein d'un constructeur qui est lui même une fonction non ? Et bien, une fonction est aussi un type d'objet — vous pouvez jeter un  œil à la documentation du constructeur Function() si vous ne nous croyez pas.

  1. Vous pouvez vérifier les attributs prototypes en reprenant l'exempe précédent et en entrant le code suivant dans la console JavaScript :
    Personne.prototype
  2. Il n'y a pas grand chose renvoyé par le navigateur, en même temps nous n'avons rien défini dans l'attribut prototype de notre constructeur et par défaut l'attribut prototype d'un constructeur est toujours vide. Voyons ce que renvoie le code suivant :
    Object.prototype

On observe que plusieurs méthodes son définies au niveau des attributs prototype d'Object comme nous l'avons vu plus haut.

Vous verrez qu'il y existe plein d'exemples de chaine de prototypage dans JavaScript. Vous pouvez essayer de trouver les méthodes et les attributs définis pour les prototype de String, Date, Number, et Array. Chacun de ces objets posède des éléments au sein de leur attribut prototype. Dès lors que l'on créé une chaine de caractères, comme celle-ci :

var maChaine = 'Ceci est ma chaine de caracteres.';

maChaine possède aussitôt pluiseures méthodes utiles pour manipuler les chaines de caractères telles que split(), indexOf(), replace()

Important : L'attribut prototype est un des éléments  JavaScript  qui peut prêter à confusion. On pourrait penser qu'il s'agit du prototype objet de l'objet courant mais ça ne l'est pas (on peut y accéder via __proto__). L'attribut prototype est un attribut qui contient un objet où l'on définit les éléments dont on va pouvoir hériter.

Revenons sur create()

Nous avons vu précedemment que la méthode Object.create() pouvait être utilisé pour instancier des objets.

  1. Par exemple, vous pouvez essayer le code suivant dans la console JavaScript :
    var personne2 = Object.create(personne1);
  2. En réalité create() se contente de créer un nouvel objet à partir d'un prototype spécifique. Dans cet exemple, personne2 est créé à partir de personne1 qui agit en tant que prototype. Vous pouvez le vérifier via :
    person2.__proto__

Cela renverra l'objet personne1.

L'attribut constructor

Chaque fonction possède un attribut prototype don la valeur est un objet contenant un attribut constructor. L'attribut constructor renvoie vers la méthode constructrice utilisée. Nous allons le voir dans la section suivant les attributs définis dans l'attribut Personne.prototype deviennent disponibles pour toutes les instances crées à partir du constructeur Personne(). De cette manière l'attribut constructor est aussi disponible au sein de personne1 et personne2.

  1. Par exemple, vous pouvez tester le code suivant :
    personne1.constructor
    personne2.constructor

    Chaque commande devrait renvoyer le constructeur Personne() étant donné qu'il a permis d'instancier ces objets.

    Une astuce qui peut s'avérer utile est d'ajouter des parenthèses à la fin de l'attribut constructor pour le transformer en méthode. Après tout, le constructeur est une fonction que l'on peut appeler si besoin. Il faut juste utiliser le mot-clé new pour signifier que l'on souhaite construire un objet.

  2. Par exemple :
    var personne3 = new personne1.constructor('Karen', 'Stephenson', 26, 'femme', ['jouer de la batterie', 'escalade']);
  3. Vous pouvez désormais essayer d'accéder aux propriété de personne3 :
    personne3.prenom
    personne3.age
    personne3.bio()

Ça fonctionne bien. A priori ce n'est pas la mnaière la plus simple de créer un objet et vous n'aurez pas à l'utiliser souvent mais ça peut vous débloquer quand vous devez créer une nouvelle instance et que vous ne disposez pas facilement du constructeur d'origine.

L'attribut constructor possède d'autres intérêts. Par exemple, si vous disposez du nom d'une instance objet vous pouvez utiliser le code suivant pour renvoyer le nom de son constructeur :

instanceName.constructor.name

Vous pouvez essayer :

personne1.constructor.name

Modifions les prototypes

Voyons au travers d'un exemple comment modifier l'attribut prototype d'un constructeur (les méthodes ajoutées au prototype seront alors disponibles pour toutes les instances créées à partir du constructeur).

  1. Revenons à notre exemple oojs-class-further-exercises.html et faisons une copie local du code source. En dessous du JavaScript existant, vous pouvez ajouter le code suivant, ce qui aura pour effet d'ajouter une nouvelle méthode à l'attribut prototype du constructeur :
    Personne.prototype.aurevoir = function() {
      alert(this.name.prenom + ' est sorti. Au revoir !');
    }
  2. Enregistrez vos modifications et chargez la page dans votre navigateur. Vous pouvez ensuite entrer le code suivant dans la console :
    personne1.aurevoir();

Vous devriez voir une fenêtre s'afficher avec un message contenant le nom de la personne. Cette fonctionalité est utile, mais là où ça devient plus intéressant c'est que la chaine de prototypage a été mis à jour dynamiquement, rendant automatiquement cette méthode disponible à l'ensemble des instances existantes.

Revoyons en détail ce qui s'est passé : tout d'abord nous avons défini le constructeur, ensuite nous avons instancié un objet à partir du constructeur, enfin nous avons ajouté une nouvelle méthode au prototype du construceur :

function Personne(prenom, nom, age, genre, interets) {

  // définition des attrbuts et des méthodes

};

var personne1 = new Personne('Tammi', 'Smith', 32, 'neutre', ['musique', 'ski', 'kickboxing']);

Personne.prototype.aurevoir= function() {
  alert(this.name.prenom + ' est sorti. Au revoir !');
}

Même si nous l'avons déclaré après; la méthode aurevoir() est disponible pour l'instance personne1. Son existence a mis à jour dynamiquement les méthodes de l'instance. Cela démontre ce que nous expliquions plus haut au sujet de la chaine de prototypage : le navigateur la parcourt de manière ascendante. Ainsi, il est possible de trouver directement les méthodes qui n'ont pas été définies au niveau de l'instance, plutôt que de les recopier au sein de l'instance. Cela nous permet de béniéficier d'un système extensible de manière simple et élégante.

Note : Si vous avez du mal à faire fonctionner cet exemple, vous pouvez jeter un œil au notre (oojs-class-prototype.html, voir la démo)

Vous verrez peu d'attributs définis au sein de l'attribut prototype, pour la simple et bonne raison que c'est assez peu pratique. Vous pourriez avoir :

Personne.prototype.nomComplet = 'Bob Smith';

Mais ce n'est pas très pratique étant donné qu'une personne ne sera peut-être pas appelée de cette manière. Il est plus cohérent de construire le nom entier en combinant le nom et le prénom :

Personne.prototype.nomComplet = this.name.prenom + ' ' + this.name.nom;

Ça ne fonctionnera toujours pas, en effet this aura une portée globale et sera pas dans le contexte de la fonction. En appelant cet attribut nous aurions alors undefined undefined. Dans les exemples précédents sur le prototype, nous arrivions à obtenir quelque chose de fonctionnel puisque nous étions au sein d'une méthode, qui sera utilisée par l'instance. Il est donc possible de définir des attributs invariables au niveau du prototype mais de manière générale, il est préférable de les définir au sein du constructeur.

En fait, on retrouve généralement la chose suivante : les attributs sont définis dans le constructeur, tandis que les méthodes sont définies au niveau du prototype. Cela rend le code plus simple à lire puisque les attributs sont groupés et les méthodes structurées en blocs distincts. Par exempe :

// Constructeur avec définition des attributs

function Test(a, b, c, d) {
  // définition des attributs
};

// Définition de la première méthode

Test.prototype.x = function() { ... }

// Définition de la seconde méthode

Test.prototype.y = function() { ... }

// etc...

Ce type d'implémentation peut être observée dans l'appli plan d'école de Piotr Zalewa par exemple.

Résumé

Cet article a tratié des prototypes objet en JavaScript, en incluant la chaine de prototypage qui permet aux objets d'hériter des propriétés d'un autre objet. Nous avons aussi vu l'attribut prototype et comment nous pouvons l'utiliser pour ajouter des méthodes au constructeur.

Dans le prochain article nous verrons comment appliquer l'héritage au niveau de deux objets spécifiques.

Étiquettes et contributeurs liés au document

 Contributeurs à cette page : Alpha
 Dernière mise à jour par : Alpha,