The precise meaning of "For each named own enumerable property name P of O"

Brendan Eich brendan at mozilla.com
Sat Oct 10 13:09:37 PDT 2009


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


More information about the es-discuss mailing list