Why?
We have Object.setPrototypeOf() but it's so slow in the general case that the messaging is basically "never use this".
However, literally every single use case I've ever encountered for it was much more narrowly scoped, not just a general "change an object's prototype to whatever". Namely, it satisfied these two invariants:
- all were around specializing an existing prototype to a different prototype that inherits from it, not changing it to a completely different shape
- all were around upgrading leafs, i.e. objects that aren't generally used as prototypes themselves in the codebase.
This is also how a custom element upgrade works: first the HTML parser creates an HTMLElement instance when encountering an unknown hyphenated element, then once a definition is registered current and future instances get "upgraded" to a subclass of HTMLElement . Except, instead of referencing an ES concept, this is defined somewhat ad hoc in the HTML spec. A first-class language concept could even allow generalizing this concept, opening up a lot of nice use cases around lazily upgrading instances as needed (more broadly, not just around DOM use cases).
My hypothesis is that a separate function that preserves these invariants (and throws if they are not followed):
- More clearly communicates intent. You no longer need to reach for a machete when a nail file would do. E.g. I can imagine style guides allowing
Object.upgrade()but forbiddingObject.setPrototypeOf(). - Prevents unintentional footguns.
- Could potentially be implemented more efficiently. AFAIK a lot of the cost of
Object.setPrototypeOf()is around the ripple effect of mutating an object that is itself a prototype of other objects, and the prototype chain being unstable.Object.upgrade()guarantees that objects are only ever extended at the very end, their shape only ever extended rather than mutated โ no different than adding a bunch of properties.
How?
For the first invariant (specializing, not replacing), the behavior would go a bit like this (illustrative; assuming no methods are shadowed):
Object.upgrade(obj, proto) {
const cur = Object.getPrototypeOf(obj);
if (cur === proto) return obj; // no-op
if (cur !== null && !cur.isPrototypeOf(proto)) {
// not an extension of the current chain
throw new TypeError(/* ... */);
}
return Object.setPrototypeOf(obj, proto);
}
The second invariant (no ripple effects) is trickier. How do we guarantee that the object being upgraded is a leaf, i.e. not a prototype of anything else?
Restrict it to certain types of objects
- But any object can be a prototype of another, either manually or via
Object.create()! Reject.
- But any object can be a prototype of another, either manually or via
Restrict it to objects that are not currently prototypes (GC dependent)
- Probably too unstable, might require additional tracking by engines, exposes GC, and is not justified by use cases. Reject.
- Restrict it to objects that have never been prototypes
- New [[HasBeenPrototype]] internal property
- At least V8 already tracks this
- Problem: the committee has previously felt strongly against notifying objects that they were extended and throwing exposes this, but perhaps this is more agreeable given the much more limited surface and the cost-benefit?
For that to work, it's important that this operates on the internal [[Prototype]] and is not affected by Symbol.hasInstance and friends, otherwise we're back in the land of Anything Goes.
Thoughts? Iโd especially love to hear from implementors whether this may actually be more efficient. If folks think it may be worth it, I can turn it into a proposal pretty quickly.