You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

345 lines
9.6 KiB

=========================
Object enumeration issues
=========================
This document provides some design notes for pre-ES6 and ES6/ES7 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:
* http://code.google.com/p/chromium/issues/detail?id=2605
* http://ejohn.org/blog/javascript-in-chrome/
However, specification is quite different from implementation. All modern
implementations of ECMAScript iterate through object properties in the
order in which they were defined. Because of this the Chrome team has
deemed this to be a bug and will be fixing it.
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. ES6 and ES7
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()``:
* For-in:
- ES6 http://www.ecma-international.org/ecma-262/6.0/#sec-runtime-semantics-forin-div-ofheadevaluation-tdznames-expr-iterationkind
algorithm step 7 is taken (iterationKind is "enumerate"), target is
ToObject() coerced and ``[[Enumerate]]`` is applied.
http://www.ecma-international.org/ecma-262/6.0/#sec-ordinary-object-internal-methods-and-internal-slots-enumerate
states "The mechanics and order of enumerating the properties is not
specified but must conform to the rules specified below.".
- ES7 calls ``EnumerateObjectProperties()``,
http://www.ecma-international.org/ecma-262/7.0/#sec-enumerate-object-properties,
which has the same requirements as ES6:
"The mechanics and order of enumerating the properties is not specified
but must conform to the rules specified below.".
* Object.keys():
- ES6 http://www.ecma-international.org/ecma-262/6.0/#sec-object.keys
states: "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 4." The algorithm references EnumerableOwnNames,
http://www.ecma-international.org/ecma-262/6.0/#sec-enumerableownnames,
which states "The order of elements in the returned list is the same as the
enumeration order that is used by a for-in statement.".
- ES7 calls ``EnumerableOwnNames()``,
http://www.ecma-international.org/ecma-262/7.0/#sec-enumerableownnames,
whose step 5 says "Order the elements of names so they are in the same
relative order as would be produced by the Iterator that would be returned
if the EnumerateObjectProperties internal method was invoked with O.".
So the guarantees are the same as for ``for-in`` in ES7 too.
There are differences to ES5 in the following:
* ``Object.getOwnPropertyNames()`` (a binding already present in ES5)
- ES5 has no guarantees for key ordering:
http://www.ecma-international.org/ecma-262/5.1/#sec-15.2.3.4;
the section just states "For each named own property P of O".
- ES6 relies on ``GetOwnPropertyKeys()`` operation:
http://www.ecma-international.org/ecma-262/6.0/#sec-object.getownpropertynames
which in turn calls ``[[OwnPropertyKeys]]``
For ordinary objects, ES6 ``[[OwnPropertyKeys]]`` provides the ordering
often referred to as "the ES6 enumeration order",
http://www.ecma-international.org/ecma-262/6.0/#sec-ordinary-object-internal-methods-and-internal-slots-ownpropertykeys:
+ For each own property key P of O that is an integer index, in ascending
numeric index order ...
+ For each own property key P of O that is a String but is not an integer
index, in property creation order ...
+ For each own property key P of O that is a Symbol, in property creation
order ...
- ES7 matches ES6 and invokes ``GetOwnPropertyKeys()``.
* ``Object.getOwnPropertySymbols()`` is new in ES6 and has the same ordering
guarantees as above. In practice, because it only returns symbols, the
symbols must be returned in insertion order.
The ``[[OwnPropertyKeys]]`` ordering is what's typically referred to as the
"ES6 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.