Open littledan opened 4 years ago
Note that, a downside of this proposal which @jandem mentioned is that, you still have two Get's, for this.constructor
and then [Symbol.species]
, which have arbitrary side effects. Inserting these arbitrary side effects in the middle of previously simpler code caused security issues in the past in some engines. So, from an implementation perspective, this only solves some of the problems, having to do with processing after instantiation.
Interesting semantics.
The current proposal is overly ambitious in "all subclassing machinery delenda est". I agree with you that subclass creation in itself is a nice intuition, and that without continued support for it, given libraries like Buffer, the compat risk is an order of magnitude more.
Fortunately the test for any of these ideas is to go ahead and try to clean up a single built-in method and see how much complexity in existing engines it actually gets rid of. Could @jandem give some data for SpiderMonkey? I'll do a similar investigation in V8.
An alternative to this proposal is to still drop @@species
and only use this.constructor
as new.target
in instance methods. That's only one Get, but unfortunately still requires a protector in V8 for modifying .constructor
on instances of built-ins.
On the compat side, I've been told Microsoft tried shipping subclassing by delegating only to .constructor
in ES6 days and found that it was not compatible, and that's how we got @@species
. Can anyone from MS dig anything up? @bterlson?
All in all I'd still prefer to get rid of support for Type II subclassing entirely.
I looked a bit in V8 for the code to remove for both @littledan's original proposal of using constructor[@@species]
as new.target
and my alternative suggestion of only using constructor
.
Both don't really completely eliminate code complexity and slow paths, as you might expect, but do seem on paper to increase security guarantees by not having arbitrary subclass constructor run, as Dan said. They do removal some logic, but it is unclear to me at this point if that limited removal is worth the trouble to change from the current behavior.
The main thing is, if you call Array.prototype.map.call(not an array subclass instance), it should instantiate an Array, not this.constructor (think a NodeList, or Array with null prototype, or an object literal with indexed properties). This is fixable with this.constructor
in other ways.
So, I guess my suggestion here is in between Type I and Type II. Would it be worth giving it some treatment in the README?
Fortunately the test for any of these ideas is to go ahead and try to clean up a single built-in method and see how much complexity in existing engines it actually gets rid of. Could @jandem give some data for SpiderMonkey?
The big pieces in SM are, as far as I can tell:
@@species
lookups fast in C++ builtins (called ArraySpeciesLookup).Type I would let us remove each of these. The other options would let us simplify code, but we'd still need the basic machinery in some form. It also depends quite a lot on what ArraySpeciesCreate steps 6-10 would look like, for example the Realm stuff in step 6 is pretty unfortunate as well.
@anba, do you have any thoughts on this? (He added the @@species
lookup cache to SpiderMonkey.)
The savings will probably depend on the built-in:
For Array
I don't expect any performance improvements, because we still need to perform the constructor
and @@species
lookups (resp. use some mechanism to avoid those lookups in C++). It may make it easier to improve the slow-path, because we're now guaranteed to have actual Array
instances, but that's not really the goal of this project, is it? :-)
For TypedArray
or (Shared)ArrayBuffer
, that approach will allow to remove some extra code, because for example TypedArraySpeciesCreate
no longer needs to validate that result object is indeed a TypedArray
with the correct length and without a detached ArrayBuffer. But those checks are relatively cheap when compared to the constructor
and @@species
lookups, because the relevant intrinsics need to be JIT inlined with or without @@species
.
TypedArraySpeciesCreate
correctly, step 6 seems to be missing in both implementations. Hmm, relatively cheap may be less cheap than expected?! ;-)OK, sounds like this wouldn't be a big benefit; no need to focus on this idea.
Great writeup! I'm really excited about reconsidering this stuff. Two things occur to me:
Array
and call.map
on it. But this use of subclassing might mostly be for methods, not for constructor behaviorThese might both be solved with a simple tweak: Rather than totally abandoning the
this.constructor[@@species]
pattern, what if we continue to use it, but only to determine thenew.target
, and then we still go through the built-in constructor codepath? Similarly, for static methods, you can usethis
as thenew.target
but still go through the original constructor path. Some implications:@@species
still exists on the same things, etcReflect.construct
is already a thing--you already can't count on things whose__proto__
is something to have gone through a particular instantiation path.