No description, website, or topics provided.
Switch branches/tags
Nothing to show
Clone or download
Latest commit e1f1e61 Sep 17, 2018

README.md

Exploring for-in in modern JavaScript

Some tests for the behavior of for (a in b) ... in modern JavaScript engines. The specification leaves this almost totally unspecified, but real engines tend to be more consistent.

I suggest running these with eshost-cli with the --coalesce flag.

The main interesting cases are in the top level directory. Some others I was playing with are under misc/.

See spec issue which inspired this.

Some history

This is one of the least-specified parts of ECMAScript, which is closely related to the unfortunate fact that, historically, engines have differed wildly outside of some narrow cases. We've tried to improve things over time, but without much success. Discussions of this go back to the pre-ES3.1 days (most recently, see the unsuccessful enumeration strawman for ES2015), and have re-occurred for every major iteration of the spec since.

But ES2015 did introduce a requirement (in Reflect.ownKeys) for engines to preserve information about the insertion order of non-integer (or at least non-array) keys, and to be able to list integer keys in ascending order before any other keys. Since then, all the major engines have started using that order in the most common cases.

Real constraints

The lack of specificity in ECMA-262 does not reflect reality. In discussion going back years, implementors have observed that there are some constraints on the behavior of for-in which anyone who wants to run code on the web needs to follow. From what I can gather from discussions (see below), the most crucial of these are

  • no property name is ever returned twice
  • a property which is deleted (from the entire prototype chain) before it is returned, and never re-added, is never returned
  • for non-integer properties, properties are returned in insertion order
  • properties which are added to the base object being iterated after iteration begins are not returned.

(assuming no weirdness with proxies or mutable prototypes or whatever).

The spec requires the first of these, and can be construed to require the second, but leaves the third and fourth explicitly up to implementations. But an implementation which tried to do something unusual with any of these would quickly run into problems.

Problems for proxies

The existing spec language doesn't make much sense to me in a world with proxies. It refers to enumerable properties, properties being deleted, properties being added, "the" prototype of the object, etc. Few of these terms are defined in terms of MOP traps, and no guidance is given as to which, when, and how often those traps should be invoked. So how are we to interpret these?

Allen gives some interpretations, but these are still a bit imprecise for my taste.

While implementations all differ (see this comment or run proxy-trapped.js), there's a bit of commonality in that they all use the getOwnPropertyDescriptor trap to inspect properties (though they differ on which properties they inspect, and in what order, and also have bugs).

Interop semantics

Engines differ in an exciting variety of ways, but only in unusual cases. In particular, as long as the following hold for the duration of iteration

  • No proxies or host objects, including in the prototype chain (test)
  • Prototype chain does not change (test)
  • No property is added to something up the prototype chain (but may be added to the object itself) (test)
  • No property's enumerability changes (test, test)
  • No property is deleted and then re-added (test and SM bug)
  • No non-enumerable property shadows an enumerable one (test and V8 bug)
  • No shadowing property is deleted (test)
  • No property is deleted from something in the prototype chain (SM bug)

then all engines I have on hand (V8, SpiderMonkey, ChakraCore, JavaScriptCore, XS) conform to the reference implementation's behavior (modulo this ChakraCore bug).

The reference implementation behaves pretty much exactly like you'd expect:

function* EnumerateObjectProperties(obj) {
  const visited = new Set();
  for (const key of Reflect.ownKeys(obj)) {
    if (typeof key === "symbol") continue;
    const desc = Reflect.getOwnPropertyDescriptor(obj, key);
    if (desc) {
      visited.add(key);
      if (desc.enumerable) yield key;
    }
  }
  const proto = Reflect.getPrototypeOf(obj);
  if (proto === null) return;
  for (const protoKey of EnumerateObjectProperties(proto)) {
    if (!visited.has(protoKey)) yield protoKey;
  }
}

(Assuming no one touches the built-ins to which it refers.)

Previous discussions

(Yes, I have read every comment in every one of these threads.)

Chrome issues

FireFox issues

Esdiscuss threads

TC39 notes

TC39 issues