На порядок обхода свойств объектов в ES6 всё ещё нельзя положиться

Сегодня узнал, что текущая версия ECMAScript описывает порядок обхода свойств объектов.

В ES5 порядок обхода не был гарантирован. Это означало, что если есть:

const obj = { a: 1, b: 2, c: 3 }

и по нему итерируют с помощью for...in, нельзя было быть уверенным, что порядок будет a-b-c.

Но в ES6 зафиксировали «правильный» порядок. Он немного странный, но хоть какой-то. А потому Object.keys() или for...in на объекте из примере выше дадут ожидаемый порядок обхода — a-b-c.

Теперь усложним:

const obj = { a: 1, b: 2, d: 4 }

obj.c = 3

И если сделать:

> Object.keys(obj)
[ 'a', 'b', 'd', 'c' ]

Как видно, c идёт после d. Порядок добавления сохраняется.

Но добавим ещё свойства:

obj[5] = 5;
obj['6'] = 6;
obj[2 ** 32 - 2] = 7;
obj[2 ** 32 - 1] = 8;
obj[Symbol('9')] = 9;
obj.j = 10;

И посмотрим на результат:

> Object.keys(obj)
[
  '5', 
  '6', 
  '4294967294', 
  'a', 
  'b', 
  'd', 
  'c', 
  '4294967295', 
  'j',
]

Какое-то месиво.

Дело в том, что по спецификации обход собственных свойств объекта происходит так:

  1. Сначала идут ключи, похожие на индексы массива, в порядке возрастания. Да, ключи объекта — это строки, но некоторые строки похожи на числа, потому выглядят как индексы массива.

  2. Затем обычные строковые ключи в порядке добавления.

  3. Затем Symbol-ключи в порядке добавления.

Однако, есть два нюанса в примере выше:

  1. 4294967294 считается индексом, а 4294967295 уже нет — потому что граница «похожих на индексы ключей» ограничена 2^32 - 1 (не включительно). Всё, что выше, — считается обычной строкой.

  2. Symbol-свойства не попадают в Object.keys, JSON.stringify, for...in и т. д. Если нужно их получить — используется Reflect.ownKeys:

    > Reflect.ownKeys(obj)
    [
      '5', 
      '6',
      '4294967294',
      'a',
      'b',
      'd',
      'c',
      '4294967295',
      'j',
      Symbol(9),
    ]
    

И последнее:

Когда объект выводится в консоль без Object.keys или подобных «обёрток», порядок может отличаться в зависимости от окружения, потому что обход по объекту в этом случае не обязательно происходит в порядке, описанном спецификацией. Не говоря уже о том, что браузеры сперва показывают «превью» объекта в консоли, и только по клику — полную версию; и даже эти два отображения могут отличаться.

Иными словами:

  1. Не стоит доверять порядку ключей в выводе объектов в консоли.

  2. И даже несмотря на спецификацию, не стоит полагаться на порядок обхода там, где это критично.

Старая, но до сих пор актуальная статья по теме — The traversal order of object properties in ES6.