Retour au Centre de connaissances
Empreinte

Ordre des clés : les entiers trient, les chaînes restent

Objets à clés mixtes : les entières trient croissant, les chaînes gardent l’insertion, lesquelles comptent comme entières, et comment JSON, Map, for-in et spread suivent l’ordre.

BotBrowser Team

Documentation

Vous voulez la documentation structurée pour Empreinte ?

Cet article fait partie de la bibliothèque éditoriale. Pour les étapes de configuration, la référence et les mises à jour continues, passez directement à la section docs.

BotBrowser prend en charge la relecture d’un fixture autorisé du même origine sous un profil déclaré et conserve des observations bornées sur l’ordre des clés qu’une page a parcourues et la chaîne qu’elle a sérialisée, mais il ne peut pas changer la façon dont un environnement ordonne les clés d’un objet ordinaire, ne peut pas faire qu’une clé entière garde l’ordre d’insertion et ne fait pas d’un ordre de clés un signal d’identité. Cette revue construit des objets à clés mixtes et les relit via Object.keys, JSON.stringify, for...in, Map et spread, puis consigne pourquoi les clés entières trient en ordre croissant tandis que les clés chaîne gardent l’ordre d’insertion, quelles clés comptent comme entières, pourquoi for...in diffère de Object.keys, et où Map et JSON s’écartent des règles de l’objet ordinaire.

Résumé

  • Les clés entières viennent toujours en premier et trient en ordre croissant, donc insérer b, 2, a, 10, 1 se relit comme ["1","2","10","b","a"] quel que soit l’ordre d’écriture.
  • Seules comptent les clés qui sont un entier canonique de 0 à 4294967294 ; 01, -1 et 1.5 sont des clés chaîne et gardent l’ordre d’insertion.
  • Les clés chaîne gardent l’ordre d’insertion, les symboles viennent après toute clé chaîne et Object.keys les ignore.
  • JSON.stringify suit le même ordre, donc un objet mixte se sérialise avec les clés entières d’abord même si elles ont été ajoutées en dernier.
  • for...in inclut les clés énumérables héritées ; Object.keys ne renvoie que les clés propres.
  • Supprimer puis ré-ajouter une clé chaîne la déplace à la fin, tandis qu’une clé entière se re-trie à sa place numérique.
  • Map garde l’ordre d’insertion même pour les clés entières, donc les mêmes clés dans un Map et dans un objet se relisent dans des ordres différents.

Sommaire

Un objet dont les clés insérées B, 2, A, 10, 1 se relit comme 1, 2, 10, B, A parce que les clés entières trient en ordre croissant tandis que les clés chaîne gardent l’ordre d’insertion ; une colonne MAP garde l’ordre d’insertion B, 2, 1, et une colonne JSON écrit les clés entières d’abord même si elles ont été ajoutées en dernier

Pourquoi les clés entières trient avant les clés chaîne

Un objet ordinaire n’itère pas les clés dans l’ordre où elles ont été écrites. L’ordre défini par le langage est : d’abord les clés entières en ordre numérique croissant, puis les clés chaîne restantes en ordre d’insertion, puis les symboles en ordre d’insertion. La sonde écrit cinq clés mêlant entières et chaînes et les relit :

Object=function Map=function node=v26.9.0
insert b,2,a,10,1 -> keys ["1","2","10","b","a"]

Les clés ont été insérées b, 2, a, 10, 1, mais Object.keys renvoie 1, 2, 10, b, a. Les trois clés entières 2, 10 et 1 viennent d’abord, triées comme des nombres de sorte que 1 précède 2 précède 10 plutôt que de trier comme du texte où 10 précéderait 2. Les deux clés chaîne b et a suivent dans l’ordre où elles ont été ajoutées. Rien n’a été réordonné à la main ; l’environnement a appliqué l’ordre de la spécification à chaque lecture.

Ce tri numérique explique pourquoi 10 avant 2 est l’intuition fausse : comme texte "10" trie avant "2", mais comme index il trie après. Un registre qui compare deux listes de clés suppose qu’elles sont comparables seulement si toutes deux ont été construites de la même façon, parce qu’un objet construit par insertion et un objet construit par JSON.parse du même texte peuvent itérer dans le même ordre tandis qu’un objet dont les clés entières ont été ajoutées plus tard les trie quand même en premier.

La conséquence pratique est qu’un ordre de clés consigné depuis un objet n’est pas l’ordre d’insertion. Qui ajoute b puis 2 et relit 2 puis b voit une réorganisation, et un diff de deux objets construits avec les mêmes clés dans des ordres d’insertion différents est vide parce que les clés entières dominent le début des deux. Un registre des clés d’un objet avec le mot « ordre d’insertion » est donc faux dès que l’objet porte des clés entières.

La réorganisation décide aussi de la première clé renvoyée par un itérateur, et un lecteur qui prend Object.keys(o)[0] comme « le premier champ posé par la page » lit le mauvais champ dès qu’une clé entière existe. Le début de la liste est la plus petite clé entière, pas la première insérée, donc le même code renvoie un champ différent pour un objet à clé numérique et pour un objet sans. C’est un vrai défaut dans un code qui déduit la priorité des champs par position, parce que la déduction n’est valable que pour des objets dont les clés sont toutes des chaînes non entières.

Quelles clés comptent comme entières

Toute chaîne qui ressemble à un nombre n’est pas une clé entière, et la frontière décide quelles clés trient et lesquelles gardent l’ordre d’insertion. La sonde écrit cinq clés qui semblent numériques mais ne sont pas des indices canoniques :

non-index keys order -> ["4294967294","01","-1","1.5","4294967295"]

4294967294 est 2^32 - 2, le plus grand indice de tableau canonique, et il trie en tête comme clé entière. Les clés 01, -1 et 1.5 ne sont pas des entiers canoniques (01 a un zéro initial, -1 est négatif, 1.5 n’est pas entier), donc ce sont des clés chaîne ordinaires et elles gardent l’ordre d’insertion après la clé entière. La clé 4294967295 est 2^32 - 1, une au-delà du plus grand indice, donc elle ne compte pas comme clé entière et reste une clé chaîne à la fin. La frontière est exacte : de 0 à 4294967294 sont des indices, et 2^32 - 1 non.

Cette frontière explique pourquoi une clé qui semble numérique peut changer en silence l’ordre d’un objet entier. Un code qui utilise un grand entier comme id et l’ajoute à un objet le place en tête en ordre numérique, tandis que le même id écrit avec un zéro initial ou un signe devient une clé chaîne qui reste là où elle a été insérée. Deux objets qu’un lecteur appellerait « les mêmes données » itèrent alors dans des ordres différents, et tout code qui lit la première clé ou dépend de la position voit une valeur différente.

La règle qui garde un code sûr est d’utiliser un Map quand l’ordre d’insertion compte et de traiter l’ordre des clés d’un objet comme non fiable pour tout sauf la règle entière-puis-chaîne. Un registre qui prétend préserver l’ordre des champs d’un serveur via un objet ordinaire est faux pour tout champ dont le nom est un entier canonique, parce que l’objet déplacera ce champ en tête quel que soit l’endroit où le serveur l’a écrit.

Comment for-in diffère de Object.keys

Deux boucles qui semblent identiques lisent un ensemble de clés différent, et la différence est l’héritage. La sonde construit un objet enfant dont le prototype porte une clé puis compare les deux itérateurs :

for...in -> ["own","inherited"]
Object.keys -> ["own"]

for...in parcourt les clés propres de l’enfant puis les clés énumérables qu’il hérite de sa chaîne de prototypes, donc il liste own et inherited. Object.keys ne renvoie que les clés chaîne énumérables propres de l’objet, donc il liste seulement own. Un objet créé avec un prototype portant des valeurs par défaut listera donc ces valeurs dans une boucle for...in et pas dans Object.keys, et un sérialiseur bâti sur for...in copiera des champs hérités dans la sortie tandis qu’un sérialiseur bâti sur Object.keys ne le fera pas.

Cet écart est la source d’une fuite subtile : une boucle for...in sur un objet qui hérite d’un prototype peuplé lit des champs que l’appelant n’a jamais posés, et un registre qui stocke la sortie du parcours attribue ces champs à l’objet. Object.keys, Object.values et Object.entries prennent la vue des clés propres, donc un registre bâti sur eux ne décrit que ce que l’objet possède. Les deux itérateurs diffèrent aussi sur les symboles : for...in ne visite jamais les clés symbole et Object.keys ne les renvoie pas non plus, donc ni l’un ni l’autre ne voit le symbole de la sonde précédente, et seul Reflect.ownKeys ou Object.getOwnPropertySymbols atteint les symboles.

Comment JSON, Map et spread suivent ou rompent l’ordre

Les sérialiseurs et conteneurs qu’une page utilise héritent ou écrasent l’ordre de l’objet ordinaire, et la sonde montre trois comportements. JSON suit l’ordre de l’objet, spread copie dans le même ordre, et un Map garde l’ordre d’insertion même pour les clés entières :

JSON.stringify order -> {"2":2,"b":1,"a":3}
JSON.stringify mixed -> {"2":"y","10":"x","z":"w"}
Map keys -> ["b","2","1"]
Object with same keys -> ["1","2","b"]
spread copy order -> ["2","b","a"]

JSON.stringify d’un objet aux clés b, 2, a écrit 2 d’abord parce que les clés entières trient en tête, donc le texte sérialisé ne reflète pas l’ordre dans lequel les champs ont été posés. L’objet mixte sérialise 2 puis 10 puis z, encore les clés entières d’abord en ordre numérique. Une page qui compare deux corps sérialisés par égalité voit les clés entières toujours à la même position de tête, ce qui masque une différence d’ordre d’insertion que l’objet aurait sinon montrée.

Les lignes Map sont le contraste qui compte. Un Map aux clés b, 2, 1 lu par [...map.keys()] renvoie b, 2, 1, l’ordre d’insertion, parce qu’un Map itère les entrées dans l’ordre où elles ont été posées et ne trie pas les clés entières. Convertir ce Map en objet avec Object.fromEntries réordonne les mêmes clés en 1, 2, b, donc un aller-retour par un objet change l’ordre qu’un Map a préservé. Un registre qui stocke un Map comme objet et le relit a remplacé en silence l’ordre d’insertion par la règle entière-puis-chaîne.

Cette divergence explique pourquoi le choix du conteneur est une décision de contrat de données, pas de style. Une page qui reçoit une liste de réglages, les stocke dans un Map pour garder l’ordre d’arrivée, puis les sérialise comme objet ordinaire pour le stockage a jeté l’ordre d’arrivée au moment de la conversion. Un lecteur qui recharge ensuite l’objet stocké et lit la première clé voit la plus petite clé entière, pas le premier réglage arrivé, et rien dans la forme stockée ne consigne lequel c’était. Garder le Map comme Map, ou stocker l’ordre des clés comme tableau explicite à côté des valeurs, est ce qui préserve le fait que l’objet a jeté.

La ligne du spread confirme que la copie garde l’ordre de l’objet : étaler {b:1, 2:2, a:3} dans un nouvel objet donne les clés 2, b, a, la clé entière d’abord. Spread copie les clés chaîne énumérables propres en ordre d’énumération, donc il préserve la réorganisation et non l’insertion d’origine. Qui construit une copie propre en étalant un objet et attend que l’ordre des champs corresponde à la source a raison seulement pour les objets sans clés entières.

Où supprimer puis ré-ajouter déplace une clé

Supprimer une clé puis l’ajouter de nouveau change sa position, et le changement dépend de si la clé est entière. La sonde supprime et ré-ajoute une clé chaîne et une clé entière :

delete+readd string -> ["b","c","a"]
delete+readd integer -> ["3","5"]

Pour l’objet à clés chaîne {a, b, c}, supprimer a et le ré-ajouter déplace a à la fin, donc l’ordre devient b, c, a. C’est la signature d’une clé ordonnée par insertion : la retirer oublie sa place, et la ré-ajouter l’annexe. Pour l’objet à clés entières aux clés 5 et 3, supprimer 3 et le ré-ajouter laisse l’ordre 3, 5, parce qu’une clé entière se re-trie à sa place numérique quel que soit le moment où elle a été ajoutée. Le comportement supprimer-puis-déplacer-à-la-fin ne s’applique qu’aux clés chaîne, et les clés entières y échappent.

Cette différence explique pourquoi une approche « supprimer la clé pour la pousser à la fin » fonctionne pour les clés chaîne et échoue pour les clés entières. Un cache qui supprime et ré-ajoute une entrée chaude pour la garder en dernier dans un objet ne fonctionne que tant que la clé est une chaîne ; dès que la clé est un entier canonique l’entrée revient d’un coup à sa position numérique et l’approche cesse de fonctionner en silence. La même asymétrie apparaît dans un tableau : les indices d’un tableau sont des clés entières, donc un tableau itère toujours en ordre d’indice, ce que la sonde confirme pour un tableau simple avec une propriété chaîne en plus.

Consigner une lecture de clés

Une lecture de clés soutient une affirmation étroite : cet environnement, cet objet, itéré avec cette méthode, a produit cette liste de clés. Six champs la gardent vérifiable, et aucun ne suppose que la liste est l’ordre d’insertion :

  1. La méthode d’itération, Object.keys, for...in, Reflect.ownKeys, JSON.stringify ou un itérateur de Map, parce que chacune renvoie un ensemble ou un ordre différent.
  2. Les clés telles que renvoyées, avec les clés entières déjà en ordre numérique, parce que la liste brute est le seul registre fidèle et un « ordre d’insertion » re-dérivé est faux dès qu’une clé entière est présente.
  3. Si l’objet a un prototype qui apporte des clés énumérables, parce que for...in les lit et Object.keys non.
  4. Si des clés symbole ont été demandées, parce que Object.keys et for...in les ignorent et seul Reflect.ownKeys les inclut.
  5. Pour un Map, l’ordre d’insertion tel que posé, parce qu’une conversion en objet et retour réordonne les clés entières.
  6. Si une clé a été supprimée puis ré-ajoutée, parce qu’une clé chaîne se déplace à la fin tandis qu’une entière se re-trie, et un registre qui l’omet ne peut pas expliquer une clé déplacée.

Ce qu’une telle lecture ne peut pas montrer, c’est quoi que ce soit sur la façon dont un serveur a produit les mêmes clés. Un serveur qui écrit du JSON dans un ordre de champs peut être re-sérialisé par un environnement client qui déplace les clés entières en tête, donc l’ordre choisi par le serveur n’est pas l’ordre que lit la page. Pour le côté copie des mêmes objets, structuredClone face à JSON montre quels types de conteneur survivent à une copie profonde et ce qu’un aller-retour JSON fait s’effondrer, et pour le texte qu’un regex parcourt sur la forme sérialisée, RegExp lastIndex et flags montre pourquoi un motif global réutilisé change sa réponse.

Deux habitudes gardent la lecture honnête avec le temps. D’abord, consignez la liste de clés brute de la méthode exacte utilisée, parce que la règle entière-puis-chaîne rend faux un ordre d’insertion supposé pour tout objet à clé numérique. Ensuite, utilisez un Map quand l’ordre d’insertion fait partie des données, parce qu’un objet ordinaire réordonne les clés entières à chaque lecture et à chaque aller-retour par JSON et spread.

Sources publiques

Conclusion

Traitez l’ordre des clés d’un objet comme la règle entière-puis-chaîne, pas l’ordre d’insertion : les clés entières trient en ordre croissant et viennent d’abord, seuls comptent les entiers canoniques de 0 à 4294967294, et 01, -1, 1.5 et 4294967295 restent des clés chaîne. Sachez que JSON.stringify et spread suivent cet ordre, que Map garde l’ordre d’insertion même pour les clés entières donc un aller-retour par un objet les réordonne, et que for...in lit des clés héritées là où Object.keys non. Supprimer puis ré-ajouter déplace une clé chaîne à la fin mais laisse une clé entière à sa place numérique. Consignez la méthode exacte et la liste de clés brute, et utilisez un Map quand l’ordre d’insertion fait partie des données, pour qu’un ordre de champs affirmé ne contredise jamais l’ordre qu’un environnement lit réellement.

Lecture suivante

Pour quels types de conteneur survivent à une copie profonde avant qu’aucune clé ne soit lue, lisez structuredClone face à JSON ; pour pourquoi un motif global réutilisé sur le texte sérialisé change sa réponse, lisez RegExp lastIndex et flags ; pour l’ordre qu’un tri donne à un tableau de ces clés, lisez Tri de tableau et comparateurs.

#Object.keys#Ordre D’énumération#JSON.stringify#Map#Ordre Des Propriétés

Cet article fait partie du guide : Qu'est-ce qu'une empreinte de navigateur ?

Faites passer BotBrowser de la recherche à la production

Utilisez ces guides pour comprendre le modèle, puis passez à la validation multi-plateforme, aux contextes isolés et au déploiement navigateur prêt pour l'échelle.