I'm looking for any indications whether or not "superclassing" a builtin type will work according to the specification. That is, given any hypothetical conformant implementation of ECMAScript, does "superclassing" a builtin break the runtime by affecting the creation algorithm of the class constructor?
"Superclassable", a term I'm coining, refers to a class whose objects returned by constructing it, or calling it as a function if applicable, will be created with the same internal slots (except for [[Prototype]]), regardless of what its direct superclass is, as long as the initial [[Prototype]] of the class constructor and the class prototype are still in each respective inheritance chain after reassigning them. Consequently, in order to be "superclassable", a class must not call super()
during creation.
When "superclassing" an Array
, I would expect it to look something like this:
// clearly this would break Array if the specification allowed an implementation
// to invoke super() internally in the Array constructor
class Enumerable {
constructor (iterator = function * () {}) {
this[Symbol.iterator] = iterator
}
asEnumerable() {
return new Enumerable(this[Symbol.iterator].bind(this))
}
}
function setSuperclassOf (Class, Superclass) {
/* These conditions must be satisfied in order to
* superclass Class with Superclass
*/
if (
!(Superclass.prototype instanceof Object.getPrototypeOf(Class.prototype).constructor) ||
!(Superclass instanceof Object.getPrototypeOf(Class).constructor) ||
(Superclass.prototype instanceof Class)
) {
throw new TypeError(`${Class.name} cannot have their superclass set to ${Superclass.name}`)
}
// Now we can superclass Class with Superclass
Object.setPrototypeOf(Class.prototype, Superclass.prototype)
Object.setPrototypeOf(Class, Superclass)
}
setSuperclassOf(Array, Enumerable)
const array = new Array(...'abc')
// Checking that Array is not broken by Enumerable
console.log(array[Symbol.iterator] === Array.prototype[Symbol.iterator])
// Checking that Enumerable works as expected
const enumerable = array.asEnumerable()
console.log(array instanceof Enumerable)
console.log(!(enumerable instanceof Array))
for (const letter of enumerable) {
console.log(letter)
}
One of my biggest concerns is that internally, in a possibly conformant implementation, Array
could potentially look like this, which would mean that Array
is not "superclassable":
class HypotheticalArray extends Object {
constructor (...values) {
const [value] = values
// this reference would be modified by superclassing HypotheticalArray
super()
if (values.length === 1) {
if (typeof value === 'number') {
if (value !== Math.floor(value) || value < 0) {
throw new RangeError('Invalid array length')
}
this.length = value
return
}
}
this.length = values.length
for (let i = 0; i < values.length; i++) {
this[i] = values[i]
}
}
* [Symbol.iterator] () {
const { length } = this
for (let i = 0; i < length; i++) {
yield this[i]
}
}
}
// Array constructor actually inherits from Function prototype, not Object constructor
Object.setPrototypeOf(HypotheticalArray, Object.getPrototypeOf(Function))
class Enumerable {
constructor (iterator = function * () {}) {
this[Symbol.iterator] = iterator
}
asEnumerable() {
return new Enumerable(this[Symbol.iterator].bind(this))
}
}
function setSuperclassOf (Class, Superclass) {
/* These conditions must be satisfied in order to
* superclass Class with Superclass
*/
if (
!(Superclass.prototype instanceof Object.getPrototypeOf(Class.prototype).constructor) ||
!(Superclass instanceof Object.getPrototypeOf(Class).constructor) ||
(Superclass.prototype instanceof Class)
) {
throw new TypeError(`${Class.name} cannot have their superclass set to ${Superclass.name}`)
}
// Now we can superclass Class with Superclass
Object.setPrototypeOf(Class.prototype, Superclass.prototype)
Object.setPrototypeOf(Class, Superclass)
}
setSuperclassOf(HypotheticalArray, Enumerable)
const array = new HypotheticalArray(...'abc')
// Array is broken by Enumerable
console.log(array[Symbol.iterator] === HypotheticalArray.prototype[Symbol.iterator])
// Checking if Enumerable works as expected
const enumerable = array.asEnumerable()
console.log(array instanceof Enumerable)
console.log(!(enumerable instanceof HypotheticalArray))
// Iteration does not work as expected
for (const letter of enumerable) {
console.log(letter)
}
However, Array
is "superclassable" if a conformant implementation is required not to call super()
:
class HypotheticalArray {
constructor (...values) {
const [value] = values
// doesn't ever invoke the superclass constructor
// super()
if (values.length === 1) {
if (typeof value === 'number') {
if (value !== Math.floor(value) || value < 0) {
throw new RangeError('Invalid array length')
}
this.length = value
return
}
}
this.length = values.length
for (let i = 0; i < values.length; i++) {
this[i] = values[i]
}
}
* [Symbol.iterator] () {
const { length } = this
for (let i = 0; i < length; i++) {
yield this[i]
}
}
}
class Enumerable {
constructor (iterator = function * () {}) {
this[Symbol.iterator] = iterator
}
asEnumerable() {
return new Enumerable(this[Symbol.iterator].bind(this))
}
}
function setSuperclassOf (Class, Superclass) {
/* These conditions must be satisfied in order to
* superclass Class with Superclass
*/
if (
!(Superclass.prototype instanceof Object.getPrototypeOf(Class.prototype).constructor) ||
!(Superclass instanceof Object.getPrototypeOf(Class).constructor) ||
(Superclass.prototype instanceof Class)
) {
throw new TypeError(`${Class.name} cannot have their superclass set to ${Superclass.name}`)
}
// Now we can superclass Class with Superclass
Object.setPrototypeOf(Class.prototype, Superclass.prototype)
Object.setPrototypeOf(Class, Superclass)
}
setSuperclassOf(HypotheticalArray, Enumerable)
const array = new HypotheticalArray(...'abc')
// Array is not broken by Enumerable
console.log(array[Symbol.iterator] === HypotheticalArray.prototype[Symbol.iterator])
// Checking if Enumerable works as expected
const enumerable = array.asEnumerable()
console.log(array instanceof Enumerable)
console.log(!(enumerable instanceof HypotheticalArray))
// Iteration works as expected
for (const letter of enumerable) {
console.log(letter)
}
With that in mind, I'd like to reference a few points from the current draft, ECMAScript 2018:
§22.1.1 The Array Constructor
The Array constructor:
- creates and initializes a new Array exotic object when called as a constructor.
- is designed to be subclassable. It may be used as the value of an extends clause of a class definition. Subclass constructors that intend to inherit the exotic Array behaviour must include a super call to the Array constructor to initialize subclass instances that are Array exotic objects.
§22.1.3 Properties of the Array Prototype Object
The Array prototype object has a [[Prototype]] internal slot whose value is the intrinsic object %ObjectPrototype%.
The Array prototype object is specified to be an Array exotic object to ensure compatibility with ECMAScript code that was created prior to the ECMAScript 2015 specification.
(emphasis added)
My understanding is that a conformant implementation is not required to internally call super()
within the Array
constructor in order to properly initialize the instance as an array exotic, nor does it require Object
to be the direct superclass of Array
(though my first quote of §22.1.3 certainly seems to imply that bit).
My question is, does the first snippet above work according to the specification, or does it only work because the currently existing implementations allow it to? i.e. is the implementation of the first HypotheticalArray
non-conformant?
And for full bounty award, I'd also like to apply this question to String
, Set
, Map
, and TypedArray
(by which I mean Object.getPrototypeOf(Uint8Array.prototype).constructor
).
I will award 500 bounty points for the first answer that rigorously addresses my questions about the practice of "superclassing" the above builtins in ECMAScript 2015 and up (the draft in which Object.setPrototypeOf()
was introduced).
I do not intend to support ECMAScript edition 5.1 and below, as modifying a builtin's inheritance chain is only possible by accessing __proto__
, which is not part of any ECMAScript specification and is therefore implementation-dependent.
P.S. I am fully aware of the reasons that practices like this are discouraged, which is why I would like to determine if the specification allows for "superclassing" without "breaking the web", as TC39 likes to say.