Retour au cours

frontend / javascript

Prototypes et chaîne de prototypage

Leçon 51 exercice

Explication

Ce que vous allez apprendre

  • Expliquer ce qu'est le prototype d'un objet et comment le moteur le consulte
  • Décrire la recherche de propriété le long de la chaîne de prototypes jusqu'à null
  • Distinguer une propriété "propre" d'une propriété héritée avec hasOwnProperty
  • Créer un lien de prototype explicite avec Object.create
  • Relier ce mécanisme à ce que fait réellement le mot-clé class, vu à la leçon suivante

Dans quel contexte ?

Un développeur debugue une fonction afficherProprietes(objet) qui boucle avec for...in et affiche bien plus de propriétés que prévu, y compris des méthodes qu'il n'a jamais définies sur cet objet précis. En creusant, il comprend que for...in parcourt aussi les propriétés héritées via la chaîne de prototypes, pas seulement les propriétés propres de l'objet. La correction : filtrer avec objet.hasOwnProperty(cle), ou utiliser Object.keys(objet) qui ne renvoie que les propriétés propres.

1. Un lien invisible entre chaque objet et un autre

Avant de découvrir les classes (prochaine leçon), il faut comprendre ce qui se cache réellement dessous : le système de prototypes. En JavaScript, chaque objet possède un lien invisible vers un autre objet, son "prototype", dont il peut utiliser les propriétés et méthodes comme si elles étaient les siennes.

Prérequis

Cette leçon suppose que vous maîtrisez déjà les objets et le destructuring (leçon précédente). Aucune connaissance des classes n'est nécessaire : elles s'appuient justement sur ce que vous allez apprendre ici.

2. Ce que fait le moteur quand une propriété est absente

Quand vous accédez à une propriété, le moteur la cherche d'abord sur l'objet lui-même. Si elle est absente, il remonte la chaîne de prototypes jusqu'à la trouver, ou jusqu'à atteindre null, la fin de la chaîne.

Étape de rechercheOù le moteur regardeRésultat si trouvé
1Propriétés propres de l'objetRenvoyée immédiatement
2Prototype direct (Object.getPrototypeOf(objet))Renvoyée si présente
3Prototype du prototype, etc.Renvoyée si présente
4null atteintundefined

3. Pourquoi ce mécanisme existe

Ce n'est pas un détail technique gratuit : c'est une économie de mémoire et un partage de comportement. Plutôt que de dupliquer une méthode dans chaque instance d'un objet, on la place une seule fois sur le prototype, et toutes les instances y accèdent par ce lien — c'est exactement ce que fait new avec une fonction constructeur.

4. Comment on faisait avant les classes ES6

Historiquement, les développeurs construisaient cet héritage "à la main" avec Object.create et des manipulations explicites de prototype, un code verbeux et sujet aux erreurs.

5. Un piège à connaître

Une confusion classique consiste à mélanger une propriété "propre" à l'objet (visible avec hasOwnProperty) et une propriété héritée du prototype (invisible pour hasOwnProperty, mais parfaitement accessible en lecture).

Piège fréquent

for...in parcourt aussi les propriétés héritées énumérables, contrairement à Object.keys() qui ne renvoie que les propriétés propres. Préférez Object.keys(), Object.entries() ou Object.values() dès que vous voulez ignorer la chaîne de prototypes.

Comprendre les prototypes n'est pas un détail académique : c'est ce qui permet de vraiment saisir pourquoi les classes ES6, vues juste après, ne sont qu'une syntaxe plus lisible posée sur ce même mécanisme, sans rien changer à son fonctionnement interne.

Commandes & code

Prototypes

Le mécanisme d'héritage natif de JavaScript, sous les classes ES6.

js
// Chaque objet a un prototype dont il herite les proprietes/methodes
const animal = {
    manger() {
        return `${this.nom} mange`;
    },
};

const chien = Object.create(animal);        // chien herite de animal via le prototype
chien.nom = "Rex";
console.log(chien.manger());                  // "Rex mange" -- methode trouvee sur le PROTOTYPE

console.log(Object.getPrototypeOf(chien) === animal);   // true
console.log(chien.hasOwnProperty("nom"));                 // true : propriete propre
console.log(chien.hasOwnProperty("manger"));               // false : heritee du prototype

// Fonctions constructeur (avant les classes ES6) : le prototype porte les methodes partagees
function Animal(nom) {
    this.nom = nom;
}
Animal.prototype.manger = function () {
    return `${this.nom} mange`;
};
Animal.prototype.espece = "inconnue";

const chat = new Animal("Felix");
console.log(chat.manger());
console.log(chat.__proto__ === Animal.prototype);    // true

// L'objet function.prototype est PARTAGE par toutes les instances -- economie memoire
const chien2 = new Animal("Buddy");
console.log(chat.manger === chien2.manger);     // true : meme fonction en memoire

// Heritage prototypal manuel (avant les classes) : le pattern historique
function Chien(nom, race) {
    Animal.call(this, nom);      // appelle le "constructeur" parent
    this.race = race;
}
Chien.prototype = Object.create(Animal.prototype);   // chaine le prototype
Chien.prototype.constructor = Chien;                   // restaure la reference au constructeur
Chien.prototype.aboyer = function () {
    return `${this.nom} aboie`;
};

const rex = new Chien("Rex", "Labrador");
console.log(rex.manger());       // methode heritee d'Animal
console.log(rex.aboyer());        // methode propre a Chien

// La chaine de prototypes complete, jusqu'a Object.prototype puis null
console.log(rex.__proto__ === Chien.prototype);                  // true
console.log(rex.__proto__.__proto__ === Animal.prototype);         // true
console.log(rex.__proto__.__proto__.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__);                             // null : fin de la chaine

// Resolution de propriete : JS remonte la chaine jusqu'a trouver la propriete (ou null)
console.log(rex.toString());       // trouve tout en haut sur Object.prototype

// instanceof : verifie la presence d'un prototype dans la chaine
console.log(rex instanceof Chien);     // true
console.log(rex instanceof Animal);      // true (heritage)
console.log(rex instanceof Object);       // true (racine de toute la chaine)

// Object.getOwnPropertyNames vs Object.keys : proprietes non-enumerables
function Compte() {
    this.solde = 100;
}
Object.defineProperty(Compte.prototype, "devise", {
    value: "EUR",
    enumerable: false,        // n'apparait pas dans for...in ou Object.keys
});
const compte = new Compte();
console.log(Object.keys(compte));                    // ["solde"]
console.log(Object.getOwnPropertyNames(compte.__proto__));   // inclut "devise"

// Pourquoi les classes ES6 existent : meme mecanisme, syntaxe bien plus lisible
class AnimalES6 {
    constructor(nom) {
        this.nom = nom;
    }
    manger() {
        return `${this.nom} mange`;
    }
}
console.log(typeof AnimalES6);                             // "function" -- les classes SONT des fonctions
console.log(AnimalES6.prototype.manger !== undefined);       // true : meme mecanisme sous le capot

Résumé

  • Chaque objet JS possède un lien vers un prototype ; la résolution de propriété remonte cette chaîne.
  • Function.prototype est partagé entre toutes les instances créées avec new : économie mémoire.
  • Object.create() crée un objet avec un prototype explicite, sans passer par un constructeur.
  • Les classes ES6 sont du sucre syntaxique au-dessus de ce même mécanisme prototypal.

Exercices pratiques

1 disponible
1

Mission : le for...in qui affiche trop de propriétés

Objectif : Diagnostiquer une fuite de propriétés héritées via la chaîne de prototypes, et reconstruire un héritage prototypal correct.

Contexte

Une fonction de debug afficherProprietes(objet) chez Technologik utilise for...in pour lister les propriétés d'un objet Chien, mais affiche aussi une méthode manger que personne n'a définie sur cet objet précis :

js
function Animal(nom) { this.nom = nom; }
Animal.prototype.manger = function () { return `${this.nom} mange`; };
function Chien(nom) { Animal.call(this, nom); }
Chien.prototype = Object.create(Animal.prototype);
const rex = new Chien("Rex");
for (const cle in rex) console.log(cle); // affiche "nom" ET "manger"

Tu dois expliquer cette fuite, corriger la fonction de debug, puis vérifier que la chaîne d'héritage est bien construite.

Résoudre l’exercice →