Dans le premier chapitre de cette section, nous avons mentionné qu'il existe des méthodes modernes pour configurer un prototype.
Définir ou lire le prototype avec obj.__proto__
est considéré comme obsolète et quelque peu obsolète (déplacé vers ce qu'on appelle « l'annexe B » de la norme JavaScript, destinée uniquement aux navigateurs).
Les méthodes modernes pour obtenir/définir un prototype sont :
Object.getPrototypeOf(obj) – renvoie le [[Prototype]]
de obj
.
Object.setPrototypeOf(obj, proto) – définit le [[Prototype]]
de obj
sur proto
.
La seule utilisation de __proto__
, qui n'est pas mal vue, est en tant que propriété lors de la création d'un nouvel objet : { __proto__: ... }
.
Cependant, il existe également une méthode spéciale pour cela :
Object.create(proto[, descriptors]) – crée un objet vide avec proto
donné comme [[Prototype]]
et des descripteurs de propriété facultatifs.
Par exemple:
soit animal = { mange : vrai } ; // crée un nouvel objet avec un animal comme prototype laissez lapin = Object.create(animal); // identique à {__proto__ : animal} alerte (lapin. mange); // vrai alert(Object.getPrototypeOf(lapin) === animal); // vrai Object.setPrototypeOf(lapin, {}); // change le prototype de lapin en {}
La méthode Object.create
est un peu plus puissante, car elle possède un deuxième argument facultatif : les descripteurs de propriété.
Nous pouvons y fournir des propriétés supplémentaires au nouvel objet, comme ceci :
soit animal = { mange : vrai } ; laissez lapin = Object.create(animal, { sauts : { valeur : vrai } }); alerte(lapin.jumps); // vrai
Les descripteurs ont le même format que celui décrit dans le chapitre Indicateurs et descripteurs de propriété.
Nous pouvons utiliser Object.create
pour effectuer un clonage d'objet plus puissant que la copie de propriétés dans for..in
:
laissez clone = Object.create( Objet.getPrototypeOf(obj), Objet.getOwnPropertyDescriptors(obj) );
Cet appel crée une copie vraiment exacte de obj
, y compris toutes les propriétés : énumérables et non énumérables, les propriétés de données et les setters/getters – tout, et avec le bon [[Prototype]]
.
Il existe de nombreuses façons de gérer [[Prototype]]
. Comment est-ce arrivé ? Pourquoi?
C'est pour des raisons historiques.
L’héritage prototypique était présent dans la langue depuis l’aube, mais les manières de le gérer ont évolué au fil du temps.
La propriété prototype
d’une fonction constructeur fonctionne depuis des temps très anciens. C'est la manière la plus ancienne de créer des objets avec un prototype donné.
Plus tard, en 2012, Object.create
est apparu dans la norme. Il donnait la possibilité de créer des objets avec un prototype donné, mais ne permettait pas de l'obtenir/le définir. Certains navigateurs ont implémenté l'accesseur non standard __proto__
qui permettait à l'utilisateur d'obtenir/définir un prototype à tout moment, pour donner plus de flexibilité aux développeurs.
Plus tard, en 2015, Object.setPrototypeOf
et Object.getPrototypeOf
ont été ajoutés à la norme, pour exécuter les mêmes fonctionnalités que __proto__
. Comme __proto__
était de facto implémenté partout, il était en quelque sorte obsolète et a fait son chemin vers l'annexe B de la norme, c'est-à-dire : facultatif pour les environnements non-navigateurs.
Plus tard, en 2022, il a été officiellement autorisé à utiliser __proto__
dans les littéraux d'objet {...}
(retirés de l'annexe B), mais pas en tant obj.__proto__
getter/setter (toujours dans l'annexe B).
Pourquoi __proto__
a-t-il été remplacé par les fonctions getPrototypeOf/setPrototypeOf
?
Pourquoi __proto__
a-t-il été partiellement réhabilité et son utilisation autorisée dans {...}
, mais pas en tant que getter/setter ?
C'est une question intéressante, qui nous oblige à comprendre pourquoi __proto__
est mauvais.
Et bientôt nous aurons la réponse.
Ne modifiez pas [[Prototype]]
sur les objets existants si la vitesse compte
Techniquement, nous pouvons obtenir/définir [[Prototype]]
à tout moment. Mais généralement, nous ne le définissons qu'une seule fois au moment de la création de l'objet et ne le modifions plus : rabbit
hérite de animal
, et cela ne va pas changer.
Et les moteurs JavaScript sont hautement optimisés pour cela. Changer un prototype « à la volée » avec Object.setPrototypeOf
ou obj.__proto__=
est une opération très lente car elle interrompt les optimisations internes pour les opérations d'accès aux propriétés d'objet. Alors évitez-le à moins que vous ne sachiez ce que vous faites, ou que la vitesse de JavaScript n'a aucune importance pour vous.
Comme nous le savons, les objets peuvent être utilisés comme tableaux associatifs pour stocker des paires clé/valeur.
… Mais si nous essayons d'y stocker les clés fournies par l'utilisateur (par exemple, un dictionnaire saisi par l'utilisateur), nous pouvons constater un problème intéressant : toutes les clés fonctionnent correctement sauf "__proto__"
.
Regardez l'exemple :
soit obj = {} ; let key = prompt("Quelle est la clé ?", "__proto__"); obj[key] = "une valeur"; alert(obj[clé]); // [objet Objet], pas "une valeur" !
Ici, si l'utilisateur tape __proto__
, l'affectation de la ligne 4 est ignorée !
Cela pourrait sûrement être surprenant pour un non-développeur, mais plutôt compréhensible pour nous. La propriété __proto__
est spéciale : elle doit être soit un objet, soit null
. Une chaîne ne peut pas devenir un prototype. C'est pourquoi l'affectation d'une chaîne à __proto__
est ignorée.
Mais nous n’avions pas l’intention de mettre en œuvre un tel comportement, n’est-ce pas ? Nous souhaitons stocker des paires clé/valeur, et la clé nommée "__proto__"
n'a pas été correctement enregistrée. C'est donc un bug !
Ici, les conséquences ne sont pas terribles. Mais dans d'autres cas, nous pouvons stocker des objets au lieu de chaînes dans obj
, et le prototype sera alors effectivement modifié. En conséquence, l’exécution se déroulera de manière totalement inattendue.
Ce qui est pire, c'est que les développeurs ne pensent généralement pas du tout à une telle possibilité. Cela rend ces bogues difficiles à remarquer et même les transforme en vulnérabilités, en particulier lorsque JavaScript est utilisé côté serveur.
Des choses inattendues peuvent également se produire lors de l'affectation à obj.toString
, car il s'agit d'une méthode objet intégrée.
Comment pouvons-nous éviter ce problème ?
Tout d’abord, nous pouvons simplement passer à l’utilisation Map
pour le stockage au lieu d’objets simples, alors tout va bien :
laissez map = new Map(); let key = prompt("Quelle est la clé ?", "__proto__"); map.set(clé, "une valeur"); alerte(map.get(clé)); // "une certaine valeur" (comme prévu)
…Mais la syntaxe Object
est souvent plus attrayante, car plus concise.
Heureusement, nous pouvons utiliser des objets, car les créateurs de langages ont réfléchi à ce problème depuis longtemps.
Comme nous le savons, __proto__
n'est pas une propriété d'un objet, mais une propriété d'accesseur de Object.prototype
:
Ainsi, si obj.__proto__
est lu ou défini, le getter/setter correspondant est appelé à partir de son prototype, et il obtient/définit [[Prototype]]
.
Comme cela a été dit au début de cette section du didacticiel : __proto__
est un moyen d'accéder [[Prototype]]
, ce n'est pas [[Prototype]]
lui-même.
Maintenant, si nous avons l'intention d'utiliser un objet comme tableau associatif et de nous libérer de tels problèmes, nous pouvons le faire avec une petite astuce :
laissez obj = Object.create(null); // ou : obj = { __proto__ : null } let key = prompt("Quelle est la clé ?", "__proto__"); obj[key] = "une valeur"; alert(obj[clé]); // "une certaine valeur"
Object.create(null)
crée un objet vide sans prototype ( [[Prototype]]
is null
) :
Il n’y a donc pas de getter/setter hérité pour __proto__
. Maintenant, il est traité comme une propriété de données normale, donc l'exemple ci-dessus fonctionne correctement.
Nous pouvons appeler de tels objets des objets « très simples » ou « purs dictionnaires », car ils sont encore plus simples que l'objet simple ordinaire {...}
.
Un inconvénient est que ces objets ne disposent pas de méthodes d'objet intégrées, par exemple toString
:
laissez obj = Object.create(null); alerte(obj); // Erreur (pas de toString)
… Mais c'est généralement bien pour les tableaux associatifs.
Notez que la plupart des méthodes liées aux objets sont Object.something(...)
, comme Object.keys(obj)
– elles ne sont pas dans le prototype, elles continueront donc à travailler sur de tels objets :
laissez ChineseDictionary = Object.create(null); ChineseDictionary.hello = "你好"; ChineseDictionary.bye = "再见"; alerte(Object.keys(chineseDictionary)); // bonjour, au revoir
Pour créer un objet avec le prototype donné, utilisez :
Object.create
fournit un moyen simple de copier superficiellement un objet avec tous les descripteurs :
laissez clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
syntaxe littérale : { __proto__: ... }
, permet de spécifier plusieurs propriétés
ou Object.create(proto[, descriptors]), permet de spécifier des descripteurs de propriétés.
Les méthodes modernes pour obtenir/définir le prototype sont :
Object.getPrototypeOf(obj) – renvoie le [[Prototype]]
de obj
(identique au getter __proto__
).
Object.setPrototypeOf(obj, proto) – définit le [[Prototype]]
de obj
sur proto
(identique au setter __proto__
).
Obtenir/définir le prototype à l'aide du getter/setter __proto__
intégré n'est pas recommandé, c'est maintenant dans l'annexe B de la spécification.
Nous avons également couvert les objets sans prototype, créés avec Object.create(null)
ou {__proto__: null}
.
Ces objets sont utilisés comme dictionnaires, pour stocker toutes les clés (éventuellement générées par l'utilisateur).
Normalement, les objets héritent des méthodes intégrées et du getter/setter __proto__
de Object.prototype
, rendant les clés correspondantes « occupées » et provoquant potentiellement des effets secondaires. Avec un prototype null
, les objets sont vraiment vides.
importance : 5
Il existe un dictionary
d'objets, créé sous le nom Object.create(null)
, pour stocker toutes les paires key/value
.
Ajoutez-y la méthode dictionary.toString()
, qui devrait renvoyer une liste de clés délimitées par des virgules. Votre toString
ne doit pas apparaître for..in
sur l'objet.
Voici comment cela devrait fonctionner :
laissez dictionnaire = Object.create(null); // votre code pour ajouter la méthode Dictionary.toString // ajoute quelques données dictionnaire.apple = "Pomme" ; dictionnaire.__proto__ = "test"; // __proto__ est une clé de propriété normale ici // seuls Apple et __proto__ sont dans la boucle pour (laisser la clé dans le dictionnaire) { alerte (clé); // "pomme", puis "__proto__" } // votre toString en action alerte (dictionnaire); // "pomme,__proto__"
La méthode peut prendre toutes les clés énumérables à l'aide de Object.keys
et afficher leur liste.
Pour rendre toString
non énumérable, définissons-le à l'aide d'un descripteur de propriété. La syntaxe de Object.create
nous permet de fournir un objet avec des descripteurs de propriétés comme deuxième argument.
laissez dictionnaire = Object.create(null, { toString : { // définit la propriété toString value() { // la valeur est une fonction return Object.keys(this).join(); } } }); dictionnaire.apple = "Pomme" ; dictionnaire.__proto__ = "test"; // apple et __proto__ sont dans la boucle pour (laisser la clé dans le dictionnaire) { alerte (clé); // "pomme", puis "__proto__" } // liste de propriétés séparées par des virgules par toString alerte (dictionnaire); // "pomme,__proto__"
Lorsque nous créons une propriété à l'aide d'un descripteur, ses indicateurs sont false
par défaut. Ainsi, dans le code ci-dessus, dictionary.toString
n'est pas énumérable.
Voir le chapitre Indicateurs et descripteurs de propriété pour examen.
importance : 5
Créons un nouvel objet rabbit
:
function Lapin(nom) { this.name = nom ; } Rabbit.prototype.sayHi = fonction() { alert(ce.nom); } ; let lapin = new Rabbit("Lapin");
Ces appels font la même chose ou pas ?
lapin.sayHi(); Lapin.prototype.sayHi(); Object.getPrototypeOf(lapin).sayHi(); lapin.__proto__.sayHi();
Le premier appel a this == rabbit
, les autres ont this
égal à Rabbit.prototype
, car c'est en fait l'objet avant le point.
Ainsi, seul le premier appel affiche Rabbit
, les autres affichent undefined
:
function Lapin(nom) { this.name = nom ; } Rabbit.prototype.sayHi = fonction() { alert( this.name ); } let lapin = new Rabbit("Lapin"); lapin.sayHi(); // Lapin Lapin.prototype.sayHi(); // non défini Object.getPrototypeOf(lapin).sayHi(); // non défini lapin.__proto__.sayHi(); // non défini