for...in loop broken in Safari

written by Tobie on January 28th, 2007 @ 09:14 PM

That browser really tires me these days...

Try the following:

var Person = function(name) {
  this.name = name;
};

Person.prototype.name = 'anonymous';

var john = new Person('John');

var result = '';
for (var property in john)
  result += property + ': ' + john[property] + '\n';

What would you expect result to look like?

"name: John"

right?

Well... not in Safari! This is what you get instead:

"name: John
 name: John"

Don't believe me? See for yourself!

Note that this has been corrected in WebKit.

Honestly, I'm not exactly sure where the problem lies, so if anyone has some input on this issue, please feel free to share.

UPDATE: I posted this on the JavaScript IRC channel. And Woosta came up with a bright explanation of the issue. A for... in loop iterates over all the properties of an object. Our object 'John' above does technically possess two 'name' properties: his own and the one inherited from his contructor. So Safari's behaviour would actually be the correct one. (Note that both properties display the value ('John') because that's the one we are targeting in the last line of code of the above example).

Woosta suggested using the hasOwnProperty method to target the properties of our object only (i.e. not of it's constructor's prototype) like so:

var Person = function(name) {
  this.name = name;
};

Person.prototype.name = 'anonymous';

var john = new Person('John');

var result = '';
for (var property in john)
  if(john.hasOwnProperty(property)) 
    result += property + ': ' + john[property] + '\n';

Surprise, surprise. This still doesn't work!

Comments

  • deltab
    deltab says:

    “Enumerating the properties of an object includes enumerating properties of its prototype, and the prototype of the prototype, and so on, recursively; but a property of a prototype is not enumerated if it is “shadowed” because some previous object in the prototype chain has a property with the same name.” — ECMA-262, §12.6.4

    Sun, January 28 at 22:35 PM

  • That second example doesn’t work because the loop receives a “name” string, checks to see if john has a property of its own by that name (which it does), and then appends john.name to result. That hasOwnProperty call doesn’t fix anything because john.name does exist on the object itself.

    The bug is as you describe it—Safari passes a shadowed property to for..in. But hasOwnProperty won’t fix it. The only way to work around that bug, as far as I can tell, is to keep track of which property names you’ve seen before and ignore them if they come up more than once.

    Sun, January 28 at 23:50 PM

  • ivan
    ivan says:

    you can simply copy key/values pairs into an assoc. array, and eliminate duplicated properties without additional testing, but yes it’s slow..

    Mon, January 29 at 10:47 AM

  • Daniel
    Daniel says:

    ECMA standard says:

    “Enumerating the properties of an object includes enumerating properties of its prototype, and the prototype of the prototype, and so on, recursively; but a property of a prototype is not enumerated if it is “shadowed” because some previous object in the prototype chain has a property with the same name.”

    Mon, January 29 at 16:51 PM

  • Daniel
    Daniel says:

    Aarrghh. I somehow missed deltab posting that until a second after I posted. Oh, for a comment delete. Sorry.

    That’s the second time I’ve done that this year. I’ll have to be more careful in the future.

    Mon, January 29 at 16:53 PM

  • Arrix
    Arrix says:

    We can find and mark the nearest position where a property is defined in the prototype chain and eliminate the shadowed properties from the result set.

    Tue, January 30 at 02:39 AM

  • JeffK
    JeffK says:

    Has anyone reported this to Apple via bugreport.apple.com?

    Tue, February 27 at 13:43 PM

  • Arrix
    Arrix says:

    Prototype 1.5.1 has a fix for this. See Hash.prototype._each().

    Wed, March 14 at 02:16 AM

Comments are closed