15

I need to loop through a JavaScript object treating it as an array with custom keys. I know this is not fully supported, since properties have no instrinsic order, but since I always reorder the properties, I found this approach simple and reliable... until now.

The problem occurs when the keys are numbers, or strings that can be casted as numbers.

When I run this code:

var test1 = {4294966222:"A",4294966333:"A",4294966111:"A"};
var test2 = {4294968222:"A",4294968333:"A",4294968111:"A"};
        
for (var k in test1) {console.log(k);}
console.log("---");
for (var k in test2) {console.log(k);}

the output is:

4294966111
4294966222
4294966333
---
4294968222
4294968333
4294968111

Which means:

  • (test1) if the keys are below 2^32 (4,294,967,296), they are automatically reordered, the smallest first
  • (test2) if the keys are above 2^32, they are NOT reordered.

The question is: why is this happening?

Since all the browsers I tested (Google Chrome 79.0, Mozilla Firefox 71.0, Microsoft Edge 44.18362, Internet Explorer 11.535) agree about this output, there must be some official specification.

Update

I tested a lot of numbers before finding out it was a threshold matter. I found odd that the sequence 2,3,1 behaves differently from three timestamps ordered in the same way.

Kar.ma
  • 743
  • 6
  • 12
  • my guess is how the hash code is calculated, but it's not a real answer to your question. – Mario Vernari Dec 19 '19 at 10:42
  • 1
    I don't think it's broken in the real colloquial sense of the word, they don't guarantee that the values will be iterated by order since it runs arbitrarily as you can check https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in "Note: for...in should not be used to iterate over an Array where the index order is important." They only guarantee iteration over every element in the collection. Something like forEach does indeed take the order into account by traversing items in ascending order http://ecma-international.org/ecma-262/5.1/#sec-15.4.4.18 – Mr.Toxy Dec 19 '19 at 10:45
  • For the record, you can see the problem directly by logging `test1` and `test2`. I think the "issue" is from the key caching in the V8 implementation of the spec. – Seblor Dec 19 '19 at 10:45
  • It is more that below 2^32 your property name is coincidentially ordered similar to the internal property references. You can and should not rely on the order of object properties since by definition they are not ordered and can contain properties that are object inherent. Always cast/map your object into an array, sort the array, then loop through it if order is important. – user3154108 Dec 19 '19 at 10:53
  • @user3154108 but forEach does run in ascending order and yet it doesn't seem to be properly working in every case check https://jsfiddle.net/9cvLfhjr/ in the first example, it traverses items correctly (in ascending order by value) but on the second example, it runs in ascending order by index. So there is something either funky or missing completely from the draft – Mr.Toxy Dec 19 '19 at 10:59
  • The point is not how I can avoid this, I does not rely on this. The point is: why is this happening? – Kar.ma Dec 19 '19 at 10:59
  • @Kar.ma check this thread https://stackoverflow.com/questions/13600922/does-javascript-array-foreach-traverse-elements-in-ascending-order and the answer provided by T.J. Crowder. apparently the behaviour of the order for in traverses the collection is not defined at all in the draft, so at this point we can only presume things. And although this behaviour is predictable in forEach it's also not documented in the draft so we can probably assume its a vendor-specific implementation – Mr.Toxy Dec 19 '19 at 11:03
  • als found this https://github.com/tc39/proposal-for-in-order its a proposal to specify how the for in traversing order works. you can check the list https://tc39.es/ its the 3rd from the bottom – Mr.Toxy Dec 19 '19 at 11:17
  • @CertainPerformance it isn't ,check github.com/tc39/proposal-for-in-order and tc39.es its the 3rd from the bottom – Mr.Toxy Dec 19 '19 at 12:45
  • @Mr.Toxy The proposal is stage 4 and is effectively already implemented everywhere. for-in iteration order *is* reliable. – CertainPerformance Dec 19 '19 at 12:46
  • @CertainPerformance interesting, however, it doesn't seem to be that reliable jsfiddle.net/9cvLfhjr an example with forEach that on the second example iterates by index order instead of value. so there's still some undocumented behaviour. – Mr.Toxy Dec 19 '19 at 12:48
  • 1
    @Mr.Toxy That's because those properties `4294968333` and `4294968111` are greater than `2 ** 32` (which is `4294967296`). So, they're not array indicies, so they're iterated over in property creation order, rather than in ascending numeric order - which is exactly what they're doing in the fiddle, as expected. (see my answer) – CertainPerformance Dec 19 '19 at 12:50
  • @Mr.Toxy forEach does traverse Arrays in order, if used on an Object it casts the object to array, and that happens with rather unpredictable (internal) array keys. That is why you map to an array yourself, bring the array into an order of your liking and then travers. Do not use forEach directly on an object when order is relevant. – user3154108 Dec 19 '19 at 13:38

2 Answers2

4

This is expected. Per the specification, the method that iterates over properties, OrdinaryOwnPropertyKeys, does:

  1. For each own property key P of O that is an array index, in ascending numeric index order, do

    a. Add P as the last element of keys.

  2. For each own property key P of O that is a String but is not an array index, in ascending chronological order of property creation, do

    a. Add P as the last element of keys.

The ascending numeric order only applies for properties which are array indicies.

So, what is an "array index"? Look it up::

An integer index is a String-valued property key that is a canonical numeric String (see 7.1.21) and whose numeric value is either +0 or a positive integer ≤ 2^53 - 1. An array index is an integer index whose numeric value i is in the range +0 ≤ i < 2^32 - 1.

So, numeric properties that are greater than 2^32 are not array indicies, and therefore iterated in order of property creation. However, numeric properties that are less than 2^32 are array indicies, and are iterated over in ascending numeric order.

So, for example:

1: Array index, will be iterated over numerically

10: Array index, will be iterated over numerically

4294968111: Greater than 2 ** 32, will be iterated over after array indicies are finished, in property creation order

9999999999999: Greater than 2 ** 32, will be iterated over after array indicies are finished, in property creation order

Also, keep in mind that, contrary to popular belief, property iteration order is guaranteed by the specification as well, thanks to the for-in iteration proposal which is stage 4.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
2

This has to do with the way they keys of an object are traversed.

According to the ES6 specifications it should be:

9.1.12 [[OwnPropertyKeys]] ( )

When the [[OwnPropertyKeys]] internal method of O is called the following steps are taken:

    Let keys be a new empty List.
    For each own property key P of O that is an integer index, in ascending numeric index order
        Add P as the last element of keys.
    For each own property key P of O that is a String but is not an integer index, in property creation order
        Add P as the last element of keys.
    For each own property key P of O that is a Symbol, in property creation order
        Add P as the last element of keys.
    Return keys.

http://www.ecma-international.org/ecma-262/6.0/#sec-ordinary-object-internal-methods-and-internal-slots-ownpropertykeys

That means if the value of a key stays the same if converted to an unsigned 53 bit number and back it is treated as an integer index which gets sorted in ascending numeric order.

If this fails it's treated as a string key, which are ordered the way they were added to the object.

The catch here is that all major browsers don't follow this specification yet and use an array index instead which is limited to a positive number up to 2^32-1. So anything above that limit is a string key actually.

obscure
  • 11,916
  • 2
  • 17
  • 36