Skip to content

Object enumeration issues

This document provides some design notes for pre-ES2015 and ES2015/ES2016 enumeration.

Key order during enumeration

ECMAScript E3 or E5 do not require a specific key order during enumeration. However, some existing code apparently relies on some ordering behavior:

User code seems to rely roughly on the following order:

  • For arrays, return all used array index keys in ascending order first
  • Then return all other keys in the order in which they have been first defined
  • The properties of the object itself are enumerated first, followed by its prototype's properties, and so on

ES5 doesn't guarantee a specific ordering for enumeration. ES2015 and ES2016 also don't guarantee a specific ordering for for-in and Object.keys() but does guarantee ordering for e.g. Object.getOwnPropertyNames().

Specification (E6 and E7)

The situation seems unchanged from ES5 for for-in and Object.keys():

There are differences to ES5 in the following:

The [[OwnPropertyKeys]] ordering is what's typically referred to as the "ES2015 enumeration order". Most engines, including Duktape 2.x, use it also for for-in and Object.keys() even if it's not required for them.

Specification (E5)

A specific ordering is not required:

  • Section 12.6.4 (The for-in statement):

    The mechanics and order of enumerating the properties [...] is not specified.

However, if an implementation provides a consistent ordering, it must do so in all relevant situations:

  • Section 15.2.3.7 (Object.defineProperties):

    If an implementation defines a specific order of enumeration for the for-in statement, that same enumeration order must be used to order the list elements in step 3 of this algorithm.

  • Section 15.2.3.14 (Object.keys):

    If an implementation defines a specific order of enumeration for the for-in statement, that same enumeration order must be used in step 5 of this algorithm.

As a side note, E5 defines a specific meaning for a "sparse" array in Section 15.4: an array is sparse essentially if it contains one or more "undefined" values in the range [0,length-1]. The "sparse" term used occasionally in the Duktape implementation is unfortunately slightly different (sparse enough to cause array part to be abandoned).

Rhino 1.7 release 2 2010 02 06

For objects (no prototype)

js> var x = {};
    x.foo = 1;
    x.bar = 1;
    x[0] = 1;
    x.quux = 1;
    x[3] = 1;
    x[1] = 1;
    x.foo = 2;
    for (var i in x) { print(i); }
foo
bar
0
quux
3
1

The behavior is consistent: all keys (including array indices) are returned in the order in which they are first defined. If a key is deleted and re-added, its enumeration order changes:

js> var x = {};
    x.foo = 1;
    x.bar = 1;
    for (var i in x) { print(i); };
foo
bar
js> delete x.foo;
    x.foo = 1;
    for (var i in x) { print(i); };
bar
foo

For arrays (no prototype)

js> var x = [];
    x.foo = 1;
    x[0] = 1;
    x[3] = 1;
    x[1] = 1;
    x.bar = 1;
    for (var i in x) { print(i); };
0
1
3
foo
bar

For small, dense arrays, the behavior is consistent: array keys (with defined values) are enumerated first, followed by keys in definition order.

However, this behavior breaks down with sparse arrays:

// still OK
js> var x = [];
    x.foo = 1;
    x[0] = 1;
    x[8] = 1;
    x[5] = 1;
    x.bar = 1;
    for (var i in x) { print(i); };
0
5
8
foo
bar

// 1000 appears after keys
js> x[1000] = 1;
    for (var i in x) { print(i); };
0
5
8
foo
bar
1000

// ... and is also followed by a newly defined key
js> x.quux = 1;
    for (var i in x) { print(i); };
0
5
8
foo
bar
1000
quux

// here '9' is higher than last well-behaving index (8) but still
// enumerates before string keys -- while '10' enumerates like
// a string key
js> x[10] = 1; x[9] = 1; for (var i in x) { print(i); };
0
5
8
9
foo
bar
1000
quux
10

Objects (with prototype)

One prototype level:

js> function F() { }
    F.prototype = { foo: 1, bar: 1 };
    x = new F();
    x.abc = 1;
    x.quux = 1;
    for (var i in x) { print(i); }
abc
quux
foo
bar

Object's keys are enumerated first, then prototype's keys. Prototype keys with same name as properties of the object are not enumerated:

js> function F() { }
    F.prototype = { foo: 1, bar: 1 };
    x = new F();
    x.quux = 1;
    x.foo = 1;
    x.xyz = 1;
    for (var i in x) { print(i); }
quux
foo
xyz
bar

Here foo is not enumerated again because it was already enumerated as part of the object's keys.

Object with an Array prototype

// test 1
js> function F() { }
    F.prototype = [1,2,3];
    x = new F();
    print("length: " + x.length);
    for (var i in x) { print(i); }
length: 3
0
1
2

// test 2
js> x[1] = 9;
    print("length: " + x.length);
    for (var i in x) { print(i); }
length: 3
1
0
2

// test 3
js> x.length = 2;  // sets enumerable own property 'length'
    print("length: " + x.length);
    for (var i in x) { print(i); }
length: 2
1
length
0
2

// test 4
js> x[10] = 10;
    print("length: " + x.length);
    for (var i in x) { print(i); }
length: 2
1
length
10
0
2

Test 1 demonstrates enumeration of an empty object whose prototype is an array of three elements. Enumeration lists the prototype keys ("0", "1", "2").

Test 2 shows that object enumeration comes first ("1") followed by prototype keys not "shadowed" by object keys ("0", "2"; "1" is shadowed).

Test 3 shows that even though the object itself is forced to be of length 2, prototype enumeration still lists all keys of the prototype, including "2" which is beyond the array length.

Test 4 shows that 'length' is not exotic for an object which has an array as a prototype. Exotic semantics of 'length' do not apply to the object because the property write goes to the object, which is not an array. This also explains the result of test 3.