Comment fonctionne la chaîne de prototypes en JavaScript
Comprenez la recherche dans la chaîne de prototypes JavaScript, la différence entre prototype et [[Prototype]], la priorité des propriétés propres et l’inspection des liens.
Lorsque vous lisez une propriété, JavaScript examine d’abord les propriétés propres de l’objet, puis suit son lien interne [[Prototype]] vers l’objet suivant. Il répète l’opération jusqu’à trouver la propriété ou atteindre null. Dans ce dernier cas, la lecture renvoie undefined.
Cette règle est simple, mais elle devient plus difficile à suivre lorsque prototype, __proto__ et [[Prototype]] apparaissent côte à côte dans un panneau des DevTools ou dans une question d’entretien technique. Firefox affiche même ce lien sous le libellé <prototype> lorsque vous dépliez un objet dans la console.
Cet article traite uniquement de la manière dont la recherche d’une propriété parcourt la chaîne : quel objet répond à une lecture, où aboutissent les écritures et quelles API utiliser pour inspecter un prototype. Pour les fonctions constructeurs, les classes et les modèles d’héritage avec Object.create, consultez A Beginner’s Guide to OOP in JavaScript et Inheritance in JavaScript Object-Oriented Programming.
Points clés
- La lecture d’une propriété examine les propriétés propres, puis chaque
[[Prototype]]tour à tour, et renvoieundefinedlorsque la chaîne se termine surnull. - La propriété
prototyped’une fonction devient le[[Prototype]]des instances créées avecnew. Ce n’est pas le prototype de la fonction elle-même. __proto__est un accesseur obsolète hérité deObject.prototype. Utilisez plutôtObject.getPrototypeOf()etObject.setPrototypeOf().- Affecter une valeur à une propriété héritée crée généralement une propriété propre qui masque la propriété héritée, sans modifier le prototype.
Object.create(null)produit un objet sans chaîne, ce qui en fait un dictionnaire sûr pour des clés de type chaîne arbitraires.
Quelle est la règle de recherche dans la chaîne de prototypes JavaScript ?
La chaîne de prototypes JavaScript est la séquence d’objets, reliés par [[Prototype]], que parcourt la lecture d’une propriété jusqu’à trouver une correspondance ou atteindre null. Tout objet ordinaire possède un emplacement interne [[Prototype]] qui pointe vers un autre objet ou vers null. L’algorithme OrdinaryGet de la spécification définit le fonctionnement d’une lecture : il recherche une propriété propre et, s’il n’en trouve pas, répète la recherche sur le prototype. Pour la plupart des objets, la chaîne se termine sur Object.prototype, dont le prototype est null.
const animal = { eats: true };
const rabbit = Object.create(animal);
rabbit.jumps = true;
console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats); // true (found on animal)
console.log(rabbit.toString === Object.prototype.toString); // true
console.log(rabbit.flies); // undefined (not found anywhere)
console.log(Object.getPrototypeOf(Object.prototype)); // null
// rabbit -> animal -> Object.prototype -> null
Lire une propriété inexistante ne provoque pas d’erreur. C’est l’appel rabbit.flies() qui lève une TypeError, car l’appel s’effectue sur undefined. Le guide Inheritance and the prototype chain de MDN présente ce même modèle de manière plus approfondie.
Quelle est la différence entre la propriété prototype d’une fonction et le [[Prototype]] d’un objet ?
La propriété prototype d’une fonction est l’objet qui devient le [[Prototype]] des instances créées avec new. Ce n’est pas le prototype de la fonction elle-même, qui est Function.prototype. Pour lire le lien réel d’un objet quelconque, utilisez Object.getPrototypeOf().
class Greeter { hello() { return 'hi'; } }
const g = new Greeter();
console.log(Object.getPrototypeOf(g) === Greeter.prototype); // true
console.log(Object.getPrototypeOf(Greeter) === Function.prototype); // true
console.log(Object.getPrototypeOf(Greeter) === Greeter.prototype); // false
const arrow = () => {};
console.log(arrow.prototype); // undefined
console.log(Greeter.prototype.hello.prototype); // undefined
Les fonctions fléchées et les méthodes de classe ordinaires n’ont aucune propriété prototype : lire .prototype sur elles renvoie donc undefined.
| Nom | Nature | Emplacement | Comment le lire |
|---|---|---|---|
[[Prototype]] | Le lien interne suivi par la recherche | Chaque objet (emplacement interne) | Object.getPrototypeOf(obj) |
obj.__proto__ | Un getter/setter historique pour ce lien | Object.prototype | À éviter ; utilisez getPrototypeOf |
Fn.prototype | L’objet attribué aux instances créées avec new Fn() | Fonctions constructibles et classes | Fn.prototype |
Qu’est-ce que proto ?
__proto__ est une propriété accesseur définie sur Object.prototype. Elle n’est pas stockée sur chaque objet : un objet qui n’hérite pas de Object.prototype ne la possède donc pas. MDN marque Object.prototype.__proto__ comme obsolète (Deprecated) et précise qu’il ne figure dans la norme que pour assurer le bon fonctionnement des anciens sites web. MDN recommande d’utiliser à la place Object.getPrototypeOf() et Object.setPrototypeOf().
const proto = { greet() { return 'hello'; } };
const obj = Object.create(proto);
console.log(Object.getPrototypeOf(obj) === proto); // true
console.log(Object.hasOwn(obj, '__proto__')); // false
console.log(Object.hasOwn(Object.prototype, '__proto__')); // true
const bare = Object.create(null);
console.log(bare.__proto__); // undefined
const lit = { __proto__: proto };
console.log(Object.getPrototypeOf(lit) === proto); // true
La clé { __proto__: value } dans un littéral d’objet est une syntaxe standard, distincte de l’accesseur. Elle n’est pas obsolète. Seule la forme avec deux-points définit le prototype. Une clé calculée ["__proto__"] crée une propriété propre ordinaire.
Même avec l’API appropriée, évitez autant que possible de modifier un prototype après la création d’un objet. MDN avertit qu’il s’agit « actuellement d’une opération très lente dans tous les navigateurs et moteurs JavaScript ». Définissez plutôt le prototype au moment de la création de l’objet.
Le masquage : les propriétés propres masquent les propriétés héritées
La recherche s’arrête à la première correspondance : une propriété propre masque donc toute propriété de même nom située plus haut dans la chaîne. L’affectation fonctionne de la même manière. Sauf si un setter hérité ou une propriété héritée non accessible en écriture fait obstacle, l’algorithme OrdinarySet de la spécification crée ou met à jour une propriété propre sur l’objet cible. La propriété héritée est masquée, pas modifiée.
const defaults = { theme: 'light' };
const settings = Object.create(defaults);
console.log(settings.theme); // 'light' (inherited)
settings.theme = 'dark'; // creates an own property
console.log(defaults.theme); // 'light' (prototype untouched)
console.log(Object.hasOwn(settings, 'theme')); // true
delete settings.theme;
console.log(settings.theme); // 'light' (inherited again)
// settings -> defaults -> Object.prototype -> null
Object.hasOwn() indique quel niveau de la chaîne a répondu à une lecture. MDN le recommande à la place de hasOwnProperty().
Comment les méthodes de Array.prototype sont-elles accessibles à tous les tableaux ?
Tout tableau créé avec un littéral de tableau a Array.prototype pour [[Prototype]]. C’est pourquoi map, filter et push fonctionnent sur tous les tableaux sans y être copiées. Les objets natifs s’appuient eux aussi sur le masquage : Array.prototype.toString masque Object.prototype.toString.
const nums = [1, 2, 3];
console.log(Object.getPrototypeOf(nums) === Array.prototype); // true
console.log(Object.hasOwn(nums, 'map')); // false
console.log(nums.map === Array.prototype.map); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
console.log(nums.toString()); // '1,2,3'
console.log(Object.prototype.toString.call(nums)); // '[object Array]'
// nums -> Array.prototype -> Object.prototype -> null
Object.create(null) pour des objets sans chaîne
Object.create(null) crée un objet dont le [[Prototype]] est null. Il n’hérite de rien : ni toString, ni hasOwnProperty, ni accesseur __proto__. Un objet à prototype nul constitue donc un conteneur sûr pour des clés issues de saisies utilisateur. Avec un simple {}, des noms hérités comme constructor renvoient déjà une valeur avant même que vous n’ayez stocké quoi que ce soit.
const words = ['a', 'constructor', 'a'];
const plain = {};
for (const w of words) plain[w] = (plain[w] ?? 0) + 1;
console.log(plain.constructor); // a string like "function Object() { [native code] }1"
const counts = Object.create(null);
for (const w of words) counts[w] = (counts[w] ?? 0) + 1;
console.log(counts.constructor); // 1
console.log(counts.a); // 2
console.log('toString' in counts); // false
// counts -> null
Un objet à prototype nul ne possède aucune méthode héritée : appelez donc Object.hasOwn(counts, key) plutôt que counts.hasOwnProperty(key). L’interpoler dans un littéral de gabarit (template literal) lève une TypeError, car aucune méthode toString ne permet de le convertir en chaîne. Lorsque les clés ne sont pas des chaînes, utilisez une Map.
Les classes utilisent-elles la même chaîne de prototypes ?
Oui. La syntaxe des classes n’introduit pas de nouveau modèle d’héritage. Les méthodes déclarées dans le corps d’une classe résident sur ClassName.prototype, et les instances y accèdent via la même recherche [[Prototype]] que n’importe quel autre objet. extends relie l’objet prototype d’une classe à celui de la classe parente.
class Animal { speak() { return 'sound'; } }
class Dog extends Animal { bark() { return 'woof'; } }
const d = new Dog();
console.log(typeof Dog); // 'function'
console.log(Object.hasOwn(d, 'speak')); // false
console.log(Object.getPrototypeOf(d) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(d.speak()); // 'sound'
// d -> Dog.prototype -> Animal.prototype -> Object.prototype -> null
Conclusion
Chaque lecture de propriété suit le même chemin. JavaScript examine l’objet lui-même, puis chaque [[Prototype]] tour à tour, jusqu’à trouver la propriété ou atteindre null. Les écritures restent sur l’objet auquel vous affectez la valeur. La prochaine fois qu’une valeur semble surgir de nulle part, exécutez Object.hasOwn() sur l’objet, puis appelez Object.getPrototypeOf() sur chaque maillon et reconstituez la chaîne jusqu’à trouver l’objet qui détient réellement la propriété.
FAQ
Quelle est la différence entre l'opérateur in et Object.hasOwn() ?
L'opérateur in examine toute la chaîne de prototypes : 'toString' in {} renvoie donc true, car toString est hérité de Object.prototype. Object.hasOwn(obj, key) n'examine que les propriétés propres de l'objet et renvoie false pour tout ce qui est hérité. Utilisez in lorsqu'une propriété héritée doit être prise en compte, et Object.hasOwn() lorsque vous devez savoir si l'objet lui-même détient la clé.
Comment instanceof utilise-t-il la chaîne de prototypes ?
Par défaut, obj instanceof C renvoie true si C.prototype apparaît n'importe où dans la chaîne de prototypes de obj. Cela ne prouve pas que C a créé l'objet, car tout objet dont la chaîne contient C.prototype satisfait la condition. Une classe peut remplacer cette vérification en définissant une méthode statique Symbol.hasInstance. Avec la vérification par défaut, une valeur primitive placée à gauche renvoie toujours false.
Les boucles for...in et Object.keys() incluent-elles les propriétés héritées ?
Une boucle for...in parcourt les propriétés énumérables à clé de type chaîne de l'objet ainsi que de chaque objet situé plus haut dans sa chaîne de prototypes. Object.keys(), Object.entries() et la décomposition d'objet (spread) n'incluent que les propriétés propres énumérables de l'objet. Les méthodes des prototypes natifs tels que Object.prototype et Array.prototype ne sont pas énumérables : elles n'apparaissent donc pas dans for...in. Les propriétés énumérables de vos propres objets prototypes, en revanche, apparaissent.
Une chaîne de prototypes JavaScript peut-elle contenir un cycle ?
Pas avec des objets ordinaires. Object.setPrototypeOf() lève une TypeError si le nouveau prototype créerait un cycle, par exemple en définissant b comme prototype de a alors que b hérite déjà de a. Une affectation via le setter __proto__ lève la même erreur. Comme les cycles sont rejetés, une chaîne d'objets ordinaires est toujours finie et se termine sur null : la recherche d'une propriété s'arrête donc toujours.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k