Héritage prototypal en JavaScript
Apprenez l'héritage prototypal JavaScript : le lien caché [[Prototype]], __proto__ vs Object.getPrototypeOf/setPrototypeOf, la chaîne de prototypes, F.prototype, le masquage de propriétés et le comportement de this.
En JavaScript, les objets peuvent hériter des propriétés et des méthodes d'autres objets grâce à un mécanisme appelé héritage prototypal. Plutôt que de copier des fonctionnalités depuis une classe (comme en Java ou en C++), chaque object conserve un lien caché vers un autre object — son prototype — et JavaScript suit ce lien chaque fois qu'il ne trouve pas une propriété sur l'object lui-même. Cette page explique comment fonctionne ce lien caché, la différence entre __proto__ et Object.getPrototypeOf/setPrototypeOf, comment la chaîne de prototypes est parcourue, le rôle de F.prototype pour les fonctions constructrices, le masquage de propriétés, et le comportement de this le long de la chaîne.
Le lien caché [[Prototype]]
Chaque object JavaScript possède une propriété interne cachée appelée [[Prototype]]. Elle est soit null, soit une référence vers un autre object, et cet object référencé est appelé le prototype de l'object.
[[Prototype]] est un slot interne défini par la spécification du langage — vous ne pouvez pas le lire avec un accès de propriété ordinaire. Vous interagissez avec lui via deux API publiques :
- L'accesseur historique
__proto__(un getter/setter exposé parObject.prototype). - Les méthodes modernes et recommandées
Object.getPrototypeOf(obj)etObject.setPrototypeOf(obj, proto).
La manière la plus simple de définir un prototype est d'utiliser __proto__ dans un littéral d'object. Ici, nous faisons d'animal le prototype de rabbit, afin que rabbit puisse lire les propriétés d'animal :
__proto__ vs Object.getPrototypeOf / setPrototypeOf
__proto__ est un getter/setter longtemps déprécié pour un usage général — il n'est standardisé que pour la compatibilité navigateur. Dans du code réel, préférez les méthodes explicites :
Notez la différence : __proto__ est un accesseur de propriété, tandis que getPrototypeOf/setPrototypeOf sont des fonctions. Deux règles pratiques :
__proto__n'est pas la même chose que[[Prototype]].__proto__est simplement l'accesseur qui lit/écrit le slot interne[[Prototype]].- Évitez de modifier un prototype après la création d'un object.
Object.setPrototypeOfet l'affectation deobj.__proto__sont des opérations lentes : les moteurs optimisent fortement les objets dont le prototype est fixé à la création. Définissez le prototype une seule fois, lors de la construction de l'object.
La chaîne de prototypes
Le prototype d'un prototype peut lui-même avoir un prototype, formant ainsi une chaîne de prototypes. Lorsque vous lisez obj.prop, JavaScript :
- Cherche
propcomme propriété propre deobj. - Si elle n'est pas trouvée, suit
[[Prototype]]et cherche sur le prototype. - Répète l'étape 2 en remontant la chaîne jusqu'à trouver
propou atteindrenull.
Si la chaîne se termine à null sans avoir trouvé la propriété, le résultat est undefined.
La chaîne est soumise à deux contraintes : les références ne doivent pas former de boucle (JavaScript lève une exception si vous tentez de créer un cycle), et [[Prototype]] doit être soit un object, soit null.
Masquage de propriétés : écriture vs lecture
Le prototype n'est consulté que pour la lecture. Lorsque vous écrivez ou supprimez une propriété, l'opération agit toujours sur l'object lui-même, jamais sur son prototype. Affecter une propriété qui existe également sur le prototype crée une propriété propre qui masque (cache) celle héritée :
L'exception concerne les propriétés accesseurs (getters/setters) : comme un setter est un appel de fonction, écrire via un setter hérité exécute ce setter plutôt que de créer une nouvelle propriété de données propre.
this est toujours l'object appelant
Une source de confusion fréquente : peu importe où une méthode se trouve dans la chaîne, this à l'intérieur est l'object avant le point lors de l'appel de la méthode — jamais le prototype sur lequel elle a été définie. Les méthodes héritées opèrent donc sur l'état propre de l'object héritant :
C'est ce qui rend les méthodes partagées sur un prototype utiles : une seule définition de méthode, mais chaque object stocke ses propres données.
Fonctions constructrices et F.prototype
Définir [[Prototype]] manuellement pour chaque object est fastidieux. Le schéma classique est celui d'une fonction constructrice utilisée avec new. Chaque fonction possède une propriété ordinaire appelée prototype (notée F.prototype). Lorsque vous appelez new F(), le [[Prototype]] du nouvel object créé est défini sur F.prototype.
Distinguez bien F.prototype du [[Prototype]] d'un object : F.prototype est une propriété ordinaire de la fonction constructrice qui fournit le [[Prototype]] aux objets créés avec new F(). Par défaut, F.prototype est un object avec une seule propriété non énumérable constructor pointant en retour vers la fonction elle-même.
Remarque : La syntaxe
classmoderne d'ES6 est du sucre syntaxique par-dessus exactement ce mécanisme. Une déclarationclasscrée une fonction constructrice, place ses méthodes surConstructor.prototype, et relie la chaîne avec les mêmes liens[[Prototype]]. Consultez JavaScript Class Inheritance pour la formeextends/super.
Créer des objets avec Object.create
Object.create(proto) construit un nouvel object dont le [[Prototype]] est directement défini sur proto — aucune fonction constructrice n'est nécessaire. C'est la manière la plus explicite de mettre en place l'héritage, et elle accepte un second argument : une carte de descripteurs de propriétés, dans le même format que celui utilisé par Object.defineProperties.
La carte de descripteurs vous permet de contrôler des indicateurs tels que writable, enumerable et configurable — consultez JavaScript Property Flags and Descriptors pour connaître le rôle de chaque indicateur. Les objets créés avec Object.create(null) (appelés objets « très simples ») sont abordés dans Prototype Methods, Objects Without __proto__.
Héritage à plusieurs niveaux
Étant donné que chaque prototype peut avoir son propre prototype, vous pouvez construire des chaînes de plusieurs niveaux de profondeur pour modéliser des relations plus spécifiques :
Inspecter et itérer la chaîne
Pour vérifier si un object se trouve quelque part dans la chaîne d'un autre object, utilisez isPrototypeOf. Pour distinguer les propriétés propres des propriétés héritées, utilisez hasOwnProperty — notez que for...in parcourt toute la chaîne (propriétés énumérables uniquement), tandis que Object.keys ne retourne que les clés propres.
Les types natifs tels que les tableaux, les fonctions et les dates s'appuient également sur cette chaîne — leurs méthodes se trouvent sur Array.prototype, Function.prototype, et ainsi de suite. Consultez JavaScript Native Prototypes pour voir comment les types natifs sont reliés entre eux.
Résumé
- Chaque object possède un
[[Prototype]]caché qui est soit un autre object, soitnull. - Lisez
[[Prototype]]avecObject.getPrototypeOf, définissez-le avecObject.setPrototypeOf;__proto__est l'accesseur historique et modifier un prototype après la création est lent. - Les lectures remontent la chaîne de prototypes jusqu'à ce que la propriété soit trouvée ou que la chaîne se termine à
null; les écritures et les suppressions agissent toujours sur l'object lui-même et peuvent masquer les propriétés héritées. thisà l'intérieur d'une méthode est l'object sur lequel la méthode a été appelée, et non le prototype sur lequel elle a été définie.new F()définit le[[Prototype]]du nouvel object surF.prototype; laclassES6 est du sucre syntaxique par-dessus ce mécanisme exact — voir JavaScript Class Inheritance.