On Oct 9, 2009, at 10:35 PM, David Flanagan wrote:
Allen Wirfs-Brock wrote:
ES5 section 12.6 (for-in statement) says:
The mechanics and order of enumerating the properties (step 6.a in
the first algorithm, step 7.a in the second) is not specified.
Properties of the object being enumerated may be deleted during
enumeration. If a property that has not yet been visited during
enumeration is deleted, then it will not be visited. If new
properties are added to the object being enumerated during
enumeration, the newly added properties are guaranteed not to be
visited in the active enumeration.
ES3 says pretty much the same thing with minor wording tweaks.
I'm resurrecting this thread from August because I just noticed that
one of the "minor wording tweaks" between the ES3 version and the
ES5 version is this one:
ES3: "the newly added properties are not guaranteed to be visited"
ES5: "the newly added properties are guaranteed not to be visited"
The transposition of the two words makes a big difference that
doesn't seem like a minor tweak to me, and I can't find a discussion
of it in the mailing lists so I wanted to make sure that this was an
intentional rather than accidental change.
The spec is ambiguously worded here. Most (all?) implementations do
not enumerate properties added to the directly referenced object after
the loop has started, even though ES1-3 permitted this as
implementation-dependent behavior:
js> o = {p:1,q:2}
({p:1, q:2})
js> for (i in o) {o.r = 3; print(i)}
p
q
js>
However, prototype objects may gain properties after the loop has
started and before the enumeration has reached the prototype in
question, and so long as the added prototype property is not shadowed,
it will be enumerated by some implementations (Mozilla's at least):
js> function f(){}
js> o = new f
({})
js> o.p = 1, o.q = 2
2
js> for (i in o) { f.prototype.r = 3; print(i); }
p
q
r
js>
(HTML source you can test is here: http://pastebin.mozilla.org/675782
-- this particular pastebin page will stick around forever.)
The spec's wording shifts subtly from "If new properties are added to
the object being enumerated during enumeration, the newly added
properties are guaranteed not to be visited in the active
enumeration", which is arguably clear enough in context (adding
properties to an object makes "own" properties in JS, and "*the*
object being enumerated" seems to mean the object that was directly
referenced by the result of evaluating the expression on the right of
'in'), to "[e]numerating the properties of an object includes
enumerating properties of its prototype, and the prototype of the
prototype, and so on, recursively [with shadowing]."
So "added to the object" means both "own" properties and the directly-
referenced object (evaluated from the expression on the right of
'in'), while "enumerating properties of an object" includes prototype
properties if not shadowed.
The old "hasOwnProperty" vs. "in" confusion bites here. The spec
should be more precise, even if context makes the two usages clear
enough for most readers.
The upshot is that the last paragraph of 12.6.4 thus may be taken to
say properties "of an object" include prototype properties, and
therefore the earlier part about added properties may be taken to
apply to added prototype properties too, specifying that they also are
not enumerated.
But in fact added prototype properties are enumerated by some
implementations so long as the enumeration has not reached the
prototype in question yet.
Perhaps this is just something for those implementations to fix to
conform to ES5, but let's discuss, since as David notes there's a lack
of discussion on this point.
To suppress prototype properties added after enumeration starts, the
implementation must in effect take a snapshot of all the enumerable
prototype property names as well as the direct names before the loop
starts, instead of snapshotting the enumerable property names only of
each object along the prototype chain as the loop progresses up the
chain.
In either case shadowing must be handled. Implementations using the
"snapshot each object while traversing once" approach may see
different shadowing effects than implementations that snapshot the
whole chain up front and traverse again during the loop:
function f(){}
o = new f;
o.p = 1, o.q = 2;
f.prototype.r = 3;
a = [];
for (i in o) {
o.r = 4;
a.push(i);
}
alert('[' + a.join(', ') + ']');
(available at http://pastebin.mozilla.org/675804). This gives
different results in different browsers although the result "[p, q]"
shows what looks to me like a bug in Mozilla's SpiderMonkey
implementation (I didn't test Rhino) where the late addition of o.r
can shadow the prototype property. I'll file it.
Another observation: using two traversals rather than one has higher
inherent cost -- is this a consideration for the spec? I don't know,
but at least it is guaranteed to visit the same objects on each
traversal in the de-jure-standardized language.
However, the writable __proto__ feature (a botch, my fault) is a de-
facto standard, and it complicates things for implementations that
support it. Do they traverse twice and risk the chain being mutated
after the loop starts but before the loop's traversal (as opposed to
the snapshotting one for all enumerable non-shadowed names)? The spec
doesn't have to worry about this but some implementations do for now
(Mozilla's, WebKit's).
And even if there's no writable __proto__, if host objects can mutate
[[Prototype]] then the possibility of inconsistent traversals arises.
Since the spec is unclear and at least Mozilla's implementation does
one traversal, and therefore shows added prototype properties so long
as they are added after the loop starts but before the enumeration
reaches the prototype in question, possibly this can remain an
unspecified, implementation-dependent behavior -- for now.
Or we can go with the ES5 change. But the trade-offs above need
deliberate discussion.
Comments welcome.
/be
_______________________________________________
es-discuss mailing list
[email protected]
https://mail.mozilla.org/listinfo/es-discuss