paulmillr / es6-shim

ECMAScript 6 compatibility shims for legacy JS engines
http://paulmillr.com
MIT License
3.11k stars 389 forks source link

Sets don't always preserve insertion order #290

Closed ndhoule closed 9 years ago

ndhoule commented 9 years ago

Per my reading of the current spec, sets should should preserve order of insertion. Most other implementations I've checked (Traceur, Firefox, Chrome Canary) do respect insertion order, but es6-shim's don't.

Repro case in Chrome 37.0.2062.124:

// Works as expected
var s1 = new Set(['d', 'a', 'b']);
Array.from(s1); //=> ['d', 'a', 'b']

// Breaks in most browsers due to no object insertion order guarantees
var s2 = new Set([3, 2, 'z', 'a', 1]);
Array.from(s2); //=> [1, 2, 3, 'z', 'a']

// Works as expected, since a non-string/number immediately triggers Map creation
var s3 = new Set([3, 2, 'z', {}, 'a', 1]);
Array.from(s3); //=> [3, 2, 'z', {}, 'a', 1]

(I'm using Array.from here for brevity, but also used the set's #forEach method with the same results.)

Seems like the root cause of this is the lazy initialization of Map; most browsers preserve object insertion order when it comes to alpha characters, but order numerics at the beginning of iteration order. So any case where numeric elements are involved discards insertion order.

I don't see any good/clean way of getting around this short of skipping the lazy initialization and just creating a Map in the first place. Thoughts on that? Happy to submit a fix and tests.

cscott commented 9 years ago

You can do the lazy initialization as soon as a numeric key is inserted, which should solve this problem. Use the existing pattern where we do lazy init in Map for non-string/number keys.

ljharb commented 9 years ago

Rather than remove the fast path for numbers, I think it might make more sense to retain an extra ordering array prior to the Map being initialized. I'll look into both approaches.

ndhoule commented 9 years ago

Cool, makes sense. I started on tests (#291) and looking at this, but sounds like @ljharb is on it.

Thanks!

ljharb commented 9 years ago

Awesome, that saves me writing a failing test :-) i'll cherry-pick that into my branch.

cscott commented 9 years ago

The extra ordering array will be very slow if keys are removed. So as long as there are no numeric keys you want to try to avoid maintaining that extra array, in order to keep deletes fast.

ljharb commented 9 years ago

It would have to do an indexOf and a splice - would that really be that slow?

Another alternative would be to continue to just use the storage map, but instead of storing true, store the numeric index of the ordering, and then use that when converting to a Map. Thoughts on that approach?

ljharb commented 9 years ago

OK, here's the two approaches I've got so far:

I'm leaning towards the second approach but I wanted to get some others' thoughts first. In either case, they make tests pass :-)

cscott commented 9 years ago

@ljharb PR #292 is my preferred approach. Use the existing 'fastkey' method to either replace numeric keys with strings, or else punt and fall back to the slow path (by returning null from fastkey). I added some feature checking to ensure we do the right thing if the engine either doesn't preserve property order at all, or only preserves order on string keys.