Does ES6 introduce a well-defined order of enumeration for object properties?
Asked Answered
V

3

108

Does ES6 introduce a well-defined order of enumeration for object properties?

var o = {
  '1': 1,
  'a': 2,
  'b': 3
}

Object.keys(o); // ["1", "a", "b"] - is this ordering guaranteed by ES6?

for(let k in o) {
  console.log(k);
} // 1 2 3 - is this ordering guaranteed by ES6?
Valina answered 6/5, 2015 at 12:5 Comment(5)
esdiscuss.org/topic/nailing-object-property-orderGrown
Btw, for Object.getOwnPropertyNames, Object.getOwnPropertySymbols and Reflect.ownKeys the order is defined.Transcaucasia
Actually - the answer is again - no longer up to date :) ES2016 introduced iteration order for Object.keys and for.. in loops and the spec: 19.1.2.16 (Object.keys) calls 7.3.21 (EnumerateOwnProperties) which in turn guarantees: "Order the elements of properties so they are in the same relative order as would be produced by the Iterator that would be returned if the EnumerateObjectProperties internal method were invoked with O." - EnumerateOwnProperties in turn guarantees [[OwnPropertyKeys]] (9.1.11) which does 9.1.11.1 (ordinaryownpropertykeys) which guarantees order.Triboelectricity
The numbers are from the ES2017 spec (8) which can be found freely here: ecma-international.org/ecma-262/8.0Triboelectricity
@BenjaminGruenbaum I don't see where 13.7.5.15 EnumerateObjectProperties guarantees the same order as [[OwnPropertyKeys]]. It only says "…must obtain the own property keys […] by calling [the] internal method". What is does with them after obtaining, or how they are merged with the inherited properties, is left to the implementation.Transcaucasia
P
122

Note: As of ES2020, even older operations like for-in and Object.keys are required to follow property order. That doesn't change the fact that using property order for fundamental program logic probably isn't a good idea, since the order for non-integer-index properties depends on when the properties were created.


Answer for ES2015-ES2019:

For for-in, Object.keys, and JSON.stringify: No.

For some other operations: Yes, usually.

While ES6 / ES2015 adds property order, it does not require for-in, Object.keys, or JSON.stringify to follow that order, due to legacy compatibility concerns.

for-in loops iterate according to [[Enumerate]], which is defined as (emphasis mine):

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

Return an Iterator object (25.1.1.2) whose next method iterates over all the String-valued keys of enumerable properties of O. The Iterator object must inherit from %IteratorPrototype% (25.1.2). The mechanics and order of enumerating the properties is not specified but must conform to the rules specified below [1].

ES7 / ES2016 removes the [[Enumerate]] internal method and instead uses the abstract operation EnumerateObjectProperties, but just like [[Enumerate]] it doesn't specify any order.

And also see this quote from Object.keys:

If an implementation defines a specific order of enumeration for the for-in statement, [...]

That means implementations are NOT required to define a specific order of enumeration. This has been confirmed by Allen Wirfs-Brock, Project Editor of the ECMAScript 2015 Language Specification, in a post made after the specification was complete.

Other operations, like Object.getOwnPropertyNames, Object.getOwnPropertySymbols, Object.defineProperties, and Reflect.ownKeys do follow the following order for ordinary objects:

  1. Integer indices (if applicable), in ascending order.
  2. Other string keys (if applicable), in property creation order.
  3. Symbol keys (if applicable), in property creation order.

This behavior is defined in the [[OwnPropertyKeys]] internal method. But certain exotic objects define that internal method slightly differently. For example, a Proxy's ownKeys trap may return an array in any order:

console.log(Reflect.ownKeys(new Proxy({}, {
  ownKeys: () => ['3','1','2']
}))); // ['3','1','2'], the integer indices are not sorted!

[1] Below it says:

[[Enumerate]] must obtain the own property keys of the target object as if by calling its [[OwnPropertyKeys]] internal method.

And the order of [[OwnPropertyKeys]] is well-defined. But don't let that confuse you: that "as if" only means "the same properties", not "the same order".

This can be seen in EnumerableOwnNames, which uses [[OwnPropertyKeys]] to get the properties, and then it orders them

in the same relative order as would be produced by the Iterator that would be returned if the [[Enumerate]] internal method was invoked

If [[Enumerate]] were required to iterate with the same order as [[OwnPropertyKeys]], there wouldn't be any need to reorder.

Pena answered 18/6, 2015 at 15:2 Comment(11)
I actually can't find info how getOwnPropertyNames does guarentee the order? And in reality Firefox and Chrome return with this Object.getOwnPropertyNames({ 20 : 'a', 10 : 'b' }) the [ "10", "20" ] numeric ordering and not the written order.Tavel
@Tavel The order of Object.getOwnPropertyNames is specified in [[OwnPropertyKeys]], and yes, it's not only by creation order.Pena
Please see my comment above on the question - this changed since 2015 @PenaTriboelectricity
Just curious how an order requirement for for-in or Object.keys would be a legacy compatibility concerns?Determine
Worth noting that while the spec doesn't require for-in or Object.keys to follow the order, current versions of Firefox, Chrome, and Edge all do: jsfiddle.net/arhbn3k2/1 Which makes sense, it would be odd to have more than one enumeration implementation. The spec didn't require it because different engines already had behavior-in-the-wild that was different from the newly-defined order and the committee didn't want to require implementations to potentially break existing code. The implementations appear to have decided to, though; Firefox's order certainly used to be different.Phallic
@Determine Haha! Great question... the answer might be that old code that should be broken would work OK... at least in that respect! This could lead, I suppose, to Exceptions not being thrown when they should be, and completely alter behaviour. I doubt whether updated specs are, or could ever be, as scrupulous as that in all cases however.Grove
@Pena I think this answer could be improved by adding a clarification at the very top, with words to the effect that objects in JavaScript should not be semantically treated as ordered.Valina
@52d6c6af - It is correct in that ES2015, specifically, didn't require Object.keys and such to follow the new order. It isn't correct in that ES2020 does now require it, not least because engines had updated (see my comment above from 2017) to do so. Since Oriol no longer contributes to SO, I've added a note to the top of the answer.Phallic
@T.J.Crowder This is a good thread and answer overall. However there is one question remaining, from my view point : how is insertion order / order of properties influenced by extensions / deprecations that vendors may ship? For example, if a vendor was to redefine the HTMLElement API or add methods to it. Would the insertion order of Object.getOwnPropertyNames(HTMLElement.prototype OR HTMLElement.prototype) be retained? Or, would these new additions / deprecations result in new ordering? This is critical for longevity of golfing via alias.Afoot
@Afoot - I'm afraid I have no idea what "golfing via alias" means, and I don't see the significance of listing HTMLElement.prototype twice like that with OR in-between. But I think I can answer the overall question: As far as I'm aware, there's no guarantee what the order of properties is in host-provided objects, it could vary vendor to vendor, or release to release. In general, you have no guarantee of the order of properties in objects you don't assemble yourself.Phallic
@T.J.Crowder Code golfing using aliases = mapping to 1-char equivalents via aliasing the APIs. For example, Han characters (3-bytes I believe?) above 45k+ in terms of char codes tends to be fairly unique with large gaps for "padding" should there be inheritance such as EventTarget->Node->HTMLElement->derivatives wherein your aliasing would otherwise be non-unique from iterative char maps. The utility of which is context dependent hence the OR depending on Object, prototype, or __proto__. And longevity is dependent on insertion order. Regardless, thanks for clarifying - what I expected.Afoot
K
15

As covered in the other answer, ES2015 does not define enumeration order for the (very commonly used) property iteration methods for-in, Object.keys, and JSON.stringify, whereas it does define enumeration methods for other methods like Reflect.ownKeys. However, this inconsistency will soon no longer exist, and all property iteration methods will iterate in a predictable manner.

As many have probably observed in their own experience with JS and in the comments, although property iteration order is not guaranteed by the specification for those methods above, every implementation almost always iterates in the same deterministic order anyway. As a result, there is a (finished) proposal to change the specification to make this behavior official:

Specifying for-in enumeration order (Stage 4)

With this proposal, under most circumstances, for..in, Object.keys / values / entries, and JSON.stringify are guaranteed to iterate in order over:

(1) Numeric array keys

(2) non-Symbol keys, in insertion order

(3) Symbol keys, in insertion order

Which is the same order as with Reflect.ownKeys and the other methods which are already guaranteed to iterate this way.

The specification text is rather simple: EnumerateObjectProperties, the problematic abstract method invoked by for..in, etc, whose order used to be unspecified, will now call [[OwnPropertyKeys]], which is the internal method for which iteration order is specified.

There are a few weird cases which implementations currently do not agree on, and in such cases, the resulting order will continue be unspecified, but such cases are few and far between.

Knopp answered 18/10, 2019 at 5:26 Comment(0)
C
11

This question is about EcmaScript 2015 (ES6). But it should be noted that the EcmaScript2017 specification has removed the following paragraph that previously appeared in the specification for Object.keys, here quoted from the EcmaScript 2016 specification:

If an implementation defines a specific order of enumeration for the for-in statement, the same order must be used for the elements of the array returned in step 3.

Furthermore, the EcmaScript 2020 specification removes the following paragraph from the section on EnumerableOwnPropertyNames, which still appears in the EcmaScript 2019 specification:

  1. Order the elements of properties so they are in the same relative order as would be produced by the Iterator that would be returned if the EnumerateObjectProperties internal method were invoked with O.

These removals mean that from EcmaScript 2020 onwards, Object.keys enforces the same specific order as Object.getOwnPropertyNames and Reflect.ownKeys, namely the one specified in OrdinaryOwnPropertyKeys. The order is:

  1. Own properties that are array indexes,1 in ascending numeric index order
  2. Other own String properties, in ascending chronological order of property creation
  3. Own Symbol properties, in ascending chronological order of property creation

1 An array index is a String-valued property key that is a canonical numeric String2 whose numeric value i is an integer in the range +0 ≤ i < 232 - 1.

2 A canonical numeric String is a String representation of a Number that would be produced by ToString, or the string "-0". So for instance, "012" is not a canonical numeric String, but "12" is.

It should be noted that all major implementations had already aligned with this order years ago.

Cardiogram answered 7/2, 2020 at 21:35 Comment(2)
Raises the question as to what order properties get when they are inserted with the same operation. E.g. object literals or Object.assign(). It might be lexical or coming from the input object in the latter case, but does it have to?Batory
@RobertSiemer - That's well-defined in the spec: Properties in an object literal are added in source code order (that's been true forever), and the behavior of Object.assign (and property spread) follows the property order of the source object(s). So Object.keys({a:1,b:2}) is (now) reliably ["a","b"] and Object.keys({b:2,a:1}) is (now) reliably ["b","a"]. But in general, best not to rely on object property iteration order. Relying on the order of creation/assignment is fine (and common: const copy = {...original, x: true}; reliably creates results in copy.x being true). :-)Phallic

© 2022 - 2024 — McMap. All rights reserved.