Назад в Центр знаний
Отпечатки

Порядок ключей объекта: целые сортируют, строки стоят

Объекты со смешанными ключами: целые сортируют по возрастанию, строковые держат вставку, какие считаются целыми, и как JSON, Map, for-in и spread следуют порядку.

BotBrowser Team

Документация

Нужна структурированная документация по теме Отпечатки?

Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.

BotBrowser поддерживает повтор authorized same-origin фикстуры под объявленным профилем и сохраняет ограниченные наблюдения о порядке ключей, которые страница обошла, и о строке, которую она сериализовала, но он не может изменить, как среда упорядочивает ключи обычного объекта, не может заставить целочисленный ключ держать порядок вставки и не превращает порядок ключей в сигнал идентичности. Этот обзор строит объекты со смешанными ключами и читает их через Object.keys, JSON.stringify, for...in, Map и spread, затем фиксирует, почему целочисленные ключи сортируют по возрастанию, а строковые держат порядок вставки, какие ключи считаются целыми, почему for...in отличается от Object.keys, и где Map и JSON расходятся с правилами обычного объекта.

Кратко

  • Целочисленные ключи всегда идут первыми и сортируют по возрастанию, поэтому вставка b, 2, a, 10, 1 читается как ["1","2","10","b","a"] независимо от порядка записи.
  • Считаются только ключи, которые есть каноническое целое от 0 до 4294967294; 01, -1 и 1.5 это строковые ключи и держат порядок вставки.
  • Строковые ключи держат порядок вставки, символы идут после любого строкового ключа, и Object.keys их пропускает.
  • JSON.stringify следует тому же порядку, поэтому смешанный объект сериализуется с целочисленными ключами первыми, даже если они добавлены последними.
  • for...in включает унаследованные перечисляемые ключи; Object.keys возвращает только собственные ключи.
  • Удаление и повторное добавление строкового ключа переносит его в конец, а целочисленный ключ пере-сортирует на свою числовую позицию.
  • Map держит порядок вставки даже для целочисленных ключей, поэтому те же ключи в Map и в объекте читаются в разных порядках.

Содержание

Объект с ключами, вставленными B, 2, A, 10, 1, читается как 1, 2, 10, B, A, потому что целочисленные ключи сортируют по возрастанию, а строковые держат порядок вставки; колонка MAP держит порядок вставки B, 2, 1, а колонка JSON пишет целочисленные ключи первыми, даже если они добавлены последними

Почему целочисленные ключи сортируют раньше строковых

Обычный объект не перебирает ключи в порядке их записи. Порядок, который задает язык: сначала целочисленные ключи по возрастанию, затем остальные строковые ключи в порядке вставки, затем символы в порядке вставки. Проба пишет пять ключей со смесью целых и строковых и читает их обратно:

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

Ключи вставлены b, 2, a, 10, 1, но Object.keys возвращает 1, 2, 10, b, a. Три целочисленных ключа 2, 10 и 1 идут первыми, отсортированные как числа так, что 1 предшествует 2 предшествует 10, а не как текст, где 10 предшествовало бы 2. Два строковых ключа b и a следуют в порядке добавления. Ничего не переупорядочено вручную; среда применяла порядок спецификации при каждом чтении.

Эта числовая сортировка объясняет, почему «10 перед 2» это неверная интуиция: как текст "10" сортирует перед "2", но как индекс сортирует после. Запись, которая сравнивает два списка ключей, предполагает, что они сравнимы только если оба построены одинаково, потому что объект, построенный вставкой, и объект, построенный через JSON.parse того же текста, могут перебираться в одном порядке, а объект, чьи целочисленные ключи добавлены позже, все равно сортирует их первыми.

Практическое следствие в том, что порядок ключей, записанный с объекта, это не порядок вставки. Тот, кто добавляет b затем 2 и читает 2 затем b, видит переупорядочивание, а diff двух объектов, построенных с теми же ключами в разных порядках вставки, пуст, потому что целочисленные ключи доминируют в начале обоих. Запись ключей объекта со словами "порядок вставки" поэтому неверна каждый раз, когда объект несет целочисленные ключи.

Переупорядочивание решает и то, какой ключ итератор вернет первым, и читатель, который берет Object.keys(o)[0] как "первое поле, которое поставила страница", читает не то поле каждый раз, когда есть целочисленный ключ. Начало списка это наименьший целочисленный ключ, а не первый вставленный, поэтому тот же код возвращает другое поле для объекта с числовым ключом и для объекта без него. Это реальный дефект в коде, который выводит приоритет полей из позиции, потому что вывод верен только для объектов, чьи ключи все нецелочисленные строки.

Какие ключи считаются целыми

Не каждая строка, похожая на число, это целочисленный ключ, и граница решает, какие ключи сортируют и какие держат порядок вставки. Проба пишет пять ключей, которые похожи на числа, но не являются каноническими индексами:

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

4294967294 это 2^32 - 2, наибольший канонический индекс массива, и он сортирует в начало как целочисленный ключ. Ключи 01, -1 и 1.5 не являются каноническими целыми (01 имеет ведущий ноль, -1 отрицателен, 1.5 не целое), поэтому это обычные строковые ключи, и они держат порядок вставки после целочисленного ключа. Ключ 4294967295 это 2^32 - 1, на единицу больше наибольшего индекса, поэтому он не считается целочисленным ключом и остается строковым в конце. Граница точна: от 0 до 4294967294 это индексы, а 2^32 - 1 нет.

Эта граница объясняет, почему ключ, похожий на число, может молча изменить порядок всего объекта. Код, который использует большое целое как id и добавляет его в объект, ставит его в начало в числовом порядке, а тот же id, записанный с ведущим нулем или знаком, становится строковым ключом, который остается там, где вставлен. Два объекта, которые читатель назвал бы "те же данные", тогда перебираются в разных порядках, и любой код, который читает первый ключ или зависит от позиции, видит другое значение.

Правило, которое держит код безопасным, это использовать Map, когда важен порядок вставки, и относиться к порядку ключей объекта как к ненадежному для всего, кроме правила целые-затем-строки. Запись, которая заявляет о сохранении порядка полей сервера через обычный объект, неверна для любого поля, чье имя есть каноническое целое, потому что объект сдвинет это поле в начало независимо от того, где его записал сервер.

Чем for-in отличается от Object.keys

Два цикла, которые кажутся одинаковыми, читают разный набор ключей, и разница в наследовании. Проба строит дочерний объект, чей прототип несет один ключ, затем сравнивает два итератора:

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

for...in обходит собственные ключи дочернего объекта, затем перечисляемые ключи, которые он наследует по цепочке прототипов, поэтому перечисляет own и inherited. Object.keys возвращает только собственные перечисляемые строковые ключи объекта, поэтому перечисляет только own. Объект, созданный с прототипом, несущим значения по умолчанию, поэтому перечислит эти значения в цикле for...in и не в Object.keys, а сериализатор, построенный на for...in, скопирует унаследованные поля в вывод, а построенный на Object.keys нет.

Этот разрыв это источник тонкой утечки: цикл for...in по объекту, который наследует от заполненного прототипа, читает поля, которые вызывающий никогда не ставил, а запись, которая хранит вывод обхода, приписывает эти поля объекту. Object.keys, Object.values и Object.entries берут вид собственных ключей, поэтому запись, построенная на них, описывает только то, чем объект владеет. Два итератора также различаются по символам: for...in никогда не посещает ключи-символы, и Object.keys их тоже не возвращает, поэтому ни один не видит символ из предыдущей пробы, и только Reflect.ownKeys или Object.getOwnPropertySymbols достигает символов.

Как JSON, Map и spread следуют или ломают порядок

Сериализаторы и контейнеры, которые использует страница, наследуют или переопределяют порядок обычного объекта, и проба показывает три поведения. JSON следует порядку объекта, spread копирует в том же порядке, а Map держит порядок вставки даже для целочисленных ключей:

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 объекта с ключами b, 2, a пишет 2 первым, потому что целочисленные ключи сортируют в начало, поэтому сериализованный текст не отражает порядок, в котором поставлены поля. Смешанный объект сериализует 2 затем 10 затем z, снова целочисленные ключи первыми в числовом порядке. Страница, которая сравнивает два сериализованных тела на равенство, видит целочисленные ключи всегда на одной и той же позиции в начале, что скрывает разницу порядка вставки, которую объект иначе показал бы.

Строки Map это контраст, который важен. Map с ключами b, 2, 1, прочитанный через [...map.keys()], возвращает b, 2, 1, порядок вставки, потому что Map перебирает записи в порядке, в котором они поставлены, и не сортирует целочисленные ключи. Преобразование этого Map в объект через Object.fromEntries переупорядочивает те же ключи в 1, 2, b, поэтому обход через объект меняет порядок, который Map сохранил. Запись, которая хранит Map как объект и читает обратно, молча заменила порядок вставки правилом целые-затем-строки.

Это расхождение объясняет, почему выбор контейнера это решение контракта данных, а не стиля. Страница, которая получает список настроек, хранит их в Map, чтобы держать порядок прибытия, а затем сериализует их как обычный объект для хранения, отбросила порядок прибытия в момент преобразования. Читатель, который затем загружает сохраненный объект и читает первый ключ, видит наименьший целочисленный ключ, а не первую прибывшую настройку, и ничто в сохраненной форме не записывает, какая это была. Держать Map как Map, или хранить порядок ключей как явный массив рядом со значениями, это то, что сохраняет факт, который объект отбросил.

Строка spread подтверждает, что копия держит порядок объекта: развертывание {b:1, 2:2, a:3} в новый объект дает ключи 2, b, a, целочисленный ключ первым. Spread копирует собственные перечисляемые строковые ключи в порядке перечисления, поэтому он сохраняет переупорядочивание, а не исходную вставку. Тот, кто строит чистую копию, развертывая объект, и ждет, что порядок полей совпадет с источником, прав только для объектов без целочисленных ключей.

Где удаление и повторное добавление двигает ключ

Удаление ключа и повторное добавление меняет его позицию, и изменение зависит от того, целочисленный ли ключ. Проба удаляет и повторно добавляет один строковый ключ и один целочисленный:

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

Для объекта со строковыми ключами {a, b, c} удаление a и повторное добавление переносит a в конец, поэтому порядок становится b, c, a. Это подпись ключа, упорядоченного вставкой: удаление забывает его слот, а повторное добавление прикрепляет его. Для объекта с целочисленными ключами 5 и 3 удаление 3 и повторное добавление оставляет порядок 3, 5, потому что целочисленный ключ пере-сортирует на свою числовую позицию независимо от того, когда добавлен. Поведение удалить-и-перенести-в-конец применяется только к строковым ключам, и целочисленные ключи от него не зависят.

Эта разница объясняет, почему подход "удалить ключ, чтобы вытолкнуть его в конец" работает для строковых ключей и падает для целочисленных. Кэш, который удаляет и повторно добавляет горячую запись, чтобы держать ее последней в объекте, работает только пока ключ строка; в момент, когда ключ есть каноническое целое, запись возвращается на свою числовую позицию, и подход молча перестает работать. Та же асимметрия появляется в массиве: индексы массива это целочисленные ключи, поэтому массив всегда перебирается в порядке индексов, что проба подтверждает для простого массива с одним лишним строковым свойством.

Записывайте показания ключей

Показания ключей поддерживают узкое утверждение: эта среда, этот объект, обойденный этим методом, произвели этот список ключей. Шесть полей сохраняют их проверяемыми, и ни одно не предполагает, что список это порядок вставки:

  1. Метод обхода, Object.keys, for...in, Reflect.ownKeys, JSON.stringify или итератор Map, потому что каждый возвращает разный набор или порядок.
  2. Ключи как возвращены, с целочисленными ключами уже в числовом порядке, потому что сырой список это единственная верная запись, а пере-выведенный "порядок вставки" неверен, как только присутствует целочисленный ключ.
  3. Есть ли у объекта прототип, который приносит перечисляемые ключи, потому что for...in их читает, а Object.keys нет.
  4. Запрашивались ли ключи-символы, потому что Object.keys и for...in их пропускают, и только Reflect.ownKeys их включает.
  5. Для Map порядок вставки как поставлен, потому что преобразование в объект и обратно переупорядочивает целочисленные ключи.
  6. Удалялся и повторно добавлялся ли какой-либо ключ, потому что строковый ключ двигается в конец, а целочисленный пере-сортирует, и запись, которая это опускает, не может объяснить сдвинутый ключ.

Чего такие показания не могут показать, так это ничего о том, как сервер произвел те же ключи. Сервер, который пишет JSON в определенном порядке полей, может быть пере-сериализован средой клиента, которая сдвигает целочисленные ключи в начало, поэтому порядок, который выбрал сервер, это не порядок, который читает страница. О стороне копии тех же объектов structuredClone против JSON показывает, какие типы контейнеров выживают при глубокой копии и что обход через JSON обрушивает, а о тексте, который regex сканирует по сериализованной форме, RegExp lastIndex и флаги показывает, почему переиспользованный global шаблон меняет ответ.

Две привычки сохраняют показания честными со временем. Во-первых, записывайте сырой список ключей из точного метода, потому что правило целые-затем-строки делает предполагаемый порядок вставки неверным для любого объекта с числовым ключом. Во-вторых, используйте Map, когда порядок вставки часть данных, потому что обычный объект переупорядочивает целочисленные ключи при каждом чтении и при каждом обходе через JSON и spread.

Публичные источники

Итоги

Относитесь к порядку ключей объекта как к правилу целые-затем-строки, а не порядку вставки: целочисленные ключи сортируют по возрастанию и идут первыми, считаются только канонические целые от 0 до 4294967294, а 01, -1, 1.5 и 4294967295 остаются строковыми ключами. Знайте, что JSON.stringify и spread следуют этому порядку, что Map держит порядок вставки даже для целочисленных ключей, поэтому обход через объект их переупорядочивает, и что for...in читает унаследованные ключи там, где Object.keys нет. Удаление и повторное добавление переносит строковый ключ в конец, но оставляет целочисленный на числовой позиции. Записывайте точный метод и сырой список ключей и используйте Map, когда порядок вставки часть данных, чтобы заявленный порядок полей никогда не противоречил порядку, который среда действительно читает.

Читать дальше

О том, какие типы контейнеров выживают при глубокой копии до того, как прочитан хоть один ключ, читайте structuredClone против JSON; о том, почему переиспользованный global шаблон по сериализованному тексту меняет ответ, читайте RegExp lastIndex и флаги; о порядке, что сортировка дает массиву этих ключей, читайте Сортировка массива и компараторы.

#Object.keys#Порядок Перечисления#JSON.stringify#Map#Порядок Свойств

Эта статья входит в руководство: Что такое отпечаток браузера?

Переведите BotBrowser из исследований в продакшн

Используйте эти руководства, чтобы понять модель, а затем перейти к кроссплатформенной валидации, изолированным контекстам и масштабируемому браузерному развертыванию.