Menu
Français

Prototypes JavaScript : la chaîne derrière chaque objet

Ce que sont vraiment les prototypes en JavaScript, comment la chaîne de prototypes résout l'accès aux propriétés, et comment la syntaxe class s'appuie sur ce même mécanisme.

Tout objet possède un prototype

JavaScript est un langage à prototypes. Ça a l'air exotique dit comme ça, mais l'idée est toute simple : chaque objet possède un lien caché vers un autre objet — son prototype — et quand on demande une propriété que l'objet n'a pas, JavaScript suit ce lien et va la chercher là-bas.

index.js
Output
Click Run to see the output here.

L'objet rabbit n'a pas de propriété eats en propre. JavaScript regarde d'abord dans rabbit, ne trouve rien, suit le lien vers le prototype animal, y trouve eats: true et le renvoie. Pour flies, il parcourt toute la chaîne, ne trouve rien, et renvoie undefined.

Ce mécanisme de recherche qui remonte la chaîne, c'est tout ce qu'il y a à comprendre. L'héritage, les méthodes, les class — tout repose là-dessus.

La chaîne de prototypes en JavaScript

La remontée ne s'arrête pas à une seule étape. Un prototype peut lui-même avoir un prototype, et ainsi de suite, jusqu'à tomber sur null :

index.js
Output
Click Run to see the output here.

Lancez ce code et vous verrez rabbit, puis Object.prototype, puis null. C'est pour ça que rabbit.toString() fonctionne alors que vous n'avez jamais défini toString : cette méthode vit sur Object.prototype, tout en haut de presque toutes les chaînes de prototypes.

La recherche de propriété remonte cette chaîne de bas en haut. En revanche, l'affectation, elle, écrit toujours directement sur l'objet lui-même : elle ne remonte jamais. Cette asymétrie est fondamentale, et elle surprend régulièrement les développeurs.

Les fonctions constructeurs et .prototype

Avant l'arrivée de class, la façon standard de créer plusieurs objets similaires consistait à utiliser une fonction constructeur appelée avec new :

index.js
Output
Click Run to see the output here.

Deux choses se produisent quand tu appelles new User("Ada") :

  1. Un nouvel objet est créé, avec son prototype défini sur User.prototype.
  2. User s'exécute avec this lié à ce nouvel objet.

greet n'est pas recopiée sur chaque instance. Elle vit à un seul endroit, sur User.prototype, et aussi bien ada que boris la retrouvent en remontant leur chaîne de prototypes. C'est pour ça que la dernière ligne affiche true — il s'agit littéralement de la même fonction.

prototype vs __proto__

Ces deux noms embrouillent tout le monde. Ils sont liés, mais ce n'est pas la même chose.

  • User.prototype est une propriété de la fonction constructeur. C'est l'objet qui devient le prototype des instances créées avec new User(...).
  • ada.__proto__ (ou Object.getPrototypeOf(ada)) est le lien porté par l'instance qui pointe vers son prototype.
index.js
Output
Click Run to see the output here.

Préférez Object.getPrototypeOf(obj) à obj.__proto__ dans le code récent. __proto__ est un accesseur historique maintenu pour des raisons de compatibilité ; c'est la fonction qui constitue l'API officielle.

Les classes ne sont que du sucre syntaxique

JavaScript moderne vous permet d'écrire class, mais en coulisses, ce sont toujours les prototypes qui font le travail. Comparez les deux versions côte à côte :

index.js
Output
Click Run to see the output here.

greet se retrouve sur User.prototype, exactement comme si tu l'avais écrit à la main. Le mot-clé class te donne surtout une syntaxe plus propre, des règles plus strictes (tu dois utiliser new) et une façon plus élégante de faire extends — mais le modèle à l'exécution reste identique.

Comprendre ça devient utile quand tu lis des messages d'erreur ou que tu débogues this. Une erreur qui mentionne « User.prototype.greet » n'est pas un nom interne bizarre — c'est littéralement l'endroit où vit la méthode.

L'héritage, ce ne sont que des chaînes de prototypes plus longues

extends relie un prototype à un autre. Le prototype du parent devient le prototype du prototype de l'enfant :

index.js
Output
Click Run to see the output here.

Quand on accède à rex.eat, JavaScript remonte la chaîne : rexDog.prototypeAnimal.prototype. Il y trouve eat et l'appelle avec this toujours lié à rex. Voilà tout ce que fait extends : il met en place la chaîne de prototypes à votre place.

Créer des objets directement à partir d'un prototype

Pas besoin de constructeur, en fait. Object.create(proto) crée un nouvel objet avec le prototype que vous lui indiquez :

index.js
Output
Click Run to see the output here.

Pas de class, pas de new, aucune fonction constructeur. Deux objets qui partagent une même méthode via un prototype commun. C'est la forme la plus brute de l'héritage prototypal en JavaScript — tout le reste est bâti par-dessus.

hasOwnProperty : propriétés propres ou héritées

Puisque la recherche remonte toute la chaîne de prototypes, "foo" in obj renvoie aussi true pour les propriétés héritées. Quand tu veux savoir si une propriété appartient vraiment à l'objet, utilise Object.hasOwn (ou l'ancien hasOwnProperty) :

index.js
Output
Click Run to see the output here.

name appartient à l'instance. greet vit sur le prototype. L'opérateur in trouve les deux, alors que Object.hasOwn ne voit que le premier. Ça devient important dès que tu parcours un objet avec for...in ou que tu le sérialises : dans la plupart des cas, tu ne veux que les propriétés propres.

Ne patche pas les prototypes natifs

Puisque Array.prototype est partagé par tous les tableaux de ton programme, tu pourrais techniquement y ajouter des méthodes :

// S'il vous plaît, ne le faites pas.
Array.prototype.last = function () {
    return this[this.length - 1];
};

[1, 2, 3].last(); // 3

Le souci, ce n'est pas que ça ne marche pas — ça fonctionne très bien. Le vrai problème, c'est que toutes les bibliothèques, toutes les dépendances et toutes les futures versions de JavaScript partagent désormais ce namespace avec vous. Le jour où Array.prototype.last finira par débarquer comme une vraie méthode native, avec une sémantique légèrement différente, votre code (ou celui d'un collègue) se cassera de manière subtile. La fameuse saga Array.prototype.flatten / Array.prototype.flat reste l'exemple à méditer.

Gardez plutôt vos utilitaires sous forme de fonctions indépendantes :

index.js
Output
Click Run to see the output here.

Une surface partagée en moins sur laquelle se marcher dessus.

Le modèle mental

Si on enlève le superflu, les prototypes tiennent en trois règles :

  • Chaque objet possède un lien vers un prototype (parfois null).
  • La lecture d'une propriété remonte la chaîne ; l'écriture, non.
  • class, new et extends ne sont que des raccourcis pour mettre en place ces chaînes sans avoir à taper Object.create à la main.

Gardez ces trois points en tête et le comportement de this, d'instanceof, de la résolution des méthodes et de l'héritage devient limpide.

La suite : la boucle d'événements

Les prototypes bouclent le chapitre sur le modèle objet. On passe maintenant à tout autre chose : comment JavaScript exécute réellement votre code dans le temps. La boucle d'événements (event loop), c'est ce qui fait que les timers, les promesses et async/await se comportent comme ils le font — et c'est la brique de base de tout ce qui est asynchrone.

Questions fréquentes

Qu'est-ce qu'un prototype en JavaScript ?

Chaque objet JavaScript possède un lien interne vers un autre objet : son prototype. Quand on accède à une propriété absente de l'objet lui-même, le moteur remonte ce lien — la fameuse chaîne de prototypes — pour la retrouver. C'est ce mécanisme qui permet à une méthode définie une seule fois d'être partagée par toutes les instances.

Quelle est la différence entre __proto__ et prototype ?

prototype est une propriété des fonctions constructeurs (et des classes). C'est l'objet qui deviendra le prototype des instances créées avec new. __proto__ (ou Object.getPrototypeOf(obj)) est le lien réel que porte une instance vers son prototype. Autrement dit : instance.__proto__ === Constructor.prototype.

Les classes JavaScript sont-elles juste du sucre syntaxique pour les prototypes ?

Pour l'essentiel, oui. class Foo { bar() {} } place bar sur Foo.prototype, exactement comme si on avait écrit function Foo(){} puis Foo.prototype.bar = function(){}. Les classes apportent les champs privés, une sémantique plus stricte et une syntaxe plus agréable pour extends et super, mais sous le capot, ce sont toujours les prototypes qui tournent.

Faut-il ajouter des méthodes aux prototypes natifs comme Array.prototype ?

Quasiment jamais. Modifier Array.prototype ou Object.prototype impacte tous les tableaux ou objets de votre programme, y compris ceux des bibliothèques tierces. Ça peut entrer en conflit avec de futurs ajouts au langage et casser vos boucles for...in. Gardez vos helpers dans des fonctions ou des modules séparés.

Apprendre à coder avec Coddy

COMMENCER