-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Normative: allow ArraySpeciesCreate to create non-arrays #1289
Conversation
Note that this revised behavior is exactly what I originally intended for ArraySpeciesCreate and the built-in methods that use it. The original version in ES6 had a bug that wasn't discovered and and subsequent refactoring were careful to preserve that buggy behavior. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good. It preserves the originally intended backwards compatibility edge cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding is that this will impact existing code in the following ways:
Borrowing an affected Array prototype method and .call
ing it on an object:
- → will now have an observable Get on "constructor"
- if that constructor is the current realm's Array, or a non-object, then this will return the same ArrayCreate as before.
- → If the constructor is a "not Array" object, it will now have an observable Get on
Symbol.species
- if that is
null
orundefined
, it will return the same ArrayCreate as before - → if that is a non-constructor, it will throw a TypeError, instead of returning ArrayCreate as before.
- If that is a constructor, it will return a constructed object per the Species-indicated constructor, likely matching the author's intention (whereas before, it would have returned ArrayCreate)
Assuming that's correct, then the spec change LGTM, pending the consensus discussion in the meeting.
@ljharb, here's a summary of (almost) all of the relevant different kinds of objects and their observable behavior when passed to ArraySpeciesCreate before and after this PR. (I'm going to ignore the cases where the object is a revocable proxy or exotic object.) Assume the object in question is named
Before:1. An object which is an array, whose
|
@bakkot
that should be Return a new exotic array object whose prototype is %ArrayPrototype% |
Rather than inferring intent from the (buggy) algorithm, let's just capture the original intent
Note that the built-in @@species property of %Array% is a get accessor that returns
Note that prior to ES6, all places that currently use ArraySpeciesCreate did the equivalent of ArrayCreate so there were no observable property accesses. The original (buggy) structuring of the ES6 ArraySpeciesCreate was primarily about implementing the above normal and special cases in a way that avoid any redundant observable property accesses. |
Whoops, thanks. Edited my original comment to avoid confusion.
Thanks, this matches my understanding except for the following caveat:
To be precise I think this should say "is an object which is not a constructor function". The algorithm still throws for, for example, |
Then that's another bug in the algorithm, because the about sequence would have been valid in ES3/5 and hence needs to be preserved. Special case 2 should probably be: The value of the original array's constructor property is null or undefined. The I'll update #1289 (comment) |
Since engines have been throwing for this case for a while now, I think it's probably fine to continue doing so, despite that breaking with ES5.
Uh, that looks like one too many lookups for a looking up 'constructor' on ? |
Yes that occurred to me to, but:
WRT "took many look ups" . Just forget about that paragraph that is trying to (poorly and unnecessarily) explain property lookup on primitive values. Instead, I think special case 2 should be:
|
Requiring Apart from the performance issue, it will also be necessary to validate that this change won't break any existing applications. Specifically when |
The committee decided not to do this, in light of potential performance and web compatibility concerns. See further discussion in #1313. |
Summary: this PR modifies the methods
Array.prototype.{concat, filter, map, slice, splice}
such that they return an instance of the same class1 as the object on which they are invoked even if that object is not an array, rather than, as currently, only in the case that the object is an array (that is, was created byArray
or a subclass thereof).1 to be precise, of the class given by
original.constructor[Symbol.species]
, if it existsArray.prototype.map
and similar methods which create new arrays defer toSymbol.species
to determine the constructor for the resulting object. However, unlike other usages ofSymbol.species
such asRegExp.prototype[Symbol.split]
, the array prototype methods will only useSymbol.species
when the object on which the method is invoked is an actual array. (I believe this may have been an oversight introduced in ES6 when specifying the magic cross-realm behavior ofArray.prototype.map
and friends.)This means that in the following code
the last line evaluates to
false
. Since Array prototype methods are explicitly intended to be usable on things which are not arrays, andSymbol.species
is explicitly intended to allow methods which create new instances to return instances of the same class as the original instance rather the class on which the method was originally defined, this is surprising to me. The current behavior also seems not particularly useful. This is especially so because a proxy for an array is treated as an array for the purposes of these algorithms, and such a proxy might not behave like an array in any way.This PR changes the behavior of the above code so that
reached
is printed and the last line evaluates totrue
.Note that this change is observable even in code which does not explicitly reference
Symbol.species
: currentlyArray.prototype.map.call(new Uint8Array([0,1,2]), x => x + 1)
returns an Array; with this change it would return aUint8Array
, just likeUint8Array.prototype.map.call(new Uint8Array([0,1,2]), x => x + 1)
.In practice, I don't expect this to affect much if any existing code. That people might be relying on the behavior in the previous paragraph is my only real worry, and I'm hopeful that's not the case. There might also be a slight performance regression when applying these methods to array-likes, as in the fairly common pattern of
[].slice.call(arguments, 0)
, since these will need to do two additional lookups (to resolvearguments.constructor[Symbol.species]
toundefined
).See twitter discussion. Also see #1178.
cc @tabatkins @allenwb