ECMAScript 规范是否允许 Array 为 "superclassable"?

Does the ECMAScript specification allow Array to be "superclassable"?

我正在寻找“superclassing”内置类型是否会根据规范工作的迹象。也就是说,给定任何假设的 ECMAScript 一致性实现,“superclassing”内置函数是否会通过影响 class 构造函数的创建算法来破坏运行时?

"Superclassable",我创造的一个术语,指的是 class 其对象通过构造它或调用它返回作为函数(如果适用),将使用相同的内部插槽创建([[Prototype]] 除外),无论其直接 superclass 是什么,只要 [[Prototype]] 的初始 [[Prototype]] =106=] 构造函数和 class 原型在重新分配后仍在各自的继承链中。因此,为了“超级class可用”,class 在创建期间不得调用 super()

当“superclassing”一个 Array 时,我希望它看起来像这样:

// 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)
}

我最大的担忧之一是,在内部,在一个可能一致的实现中,Array 可能 可能看起来像这样,这意味着 Array 不是“超级class能”:

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)
}

但是,Array“超级class能够”如果需要一致的实现不是呼叫 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)
}

考虑到这一点,我想引用当前草案中的几点,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.

(强调)

我的理解是,为了将实例正确初始化为array exotic,也不要求 ObjectArray 的直接 superclass(尽管我对 §22.1.3 的第一句话似乎暗示了这一点)。

我的问题是,上面的第一个代码片段是按照规范工作的,还是仅仅因为当前现有的实现允许它才工作?即第一个 HypotheticalArray 的实施是否不符合要求?

对于全额赏金奖励,我还想将此问题应用于 StringSetMapTypedArray(我的意思是Object.getPrototypeOf(Uint8Array.prototype).constructor).

我将为 严格地 解决我关于在 ECMAScript 2015 和向上(引入 Object.setPrototypeOf() 的草案)。

我不打算支持 ECMAScript 版本 5.1 及以下版本,因为只有通过访问 __proto__ 才能修改内置函数的继承链,而 不是 的一部分 任何 ECMAScript 规范,因此依赖于实现。

P.S。我完全知道不鼓励这样的做法的原因,这就是为什么我想确定规范是否允许“superclassing”而不“破坏网络”,正如 TC39 喜欢说的那样。

基于 §9.3.3 CreateBuiltinFunction ( steps, internalSlotsList [ , realm [ , prototype ] ] ) and the steps in §22.1.1 The Array Constructor 中概述的保证,不可能调用 Array(…)new Array(…) 将调用 Object 构造函数,或 Array 的已解析 super class 的构造函数在调用时,因此 "superclassing" Array 保证在任何符合 ECMAScript 2018 的实现中都能正常运行。

由于 §9.3.3 的发现,我怀疑当前规范中剩余的 classes 会得出相同的结论,尽管需要更多的研究来确定这是否是准确,并保证回到 ECMAScript 2015。

这不是一个完整的答案,因此我不会接受它。赏金仍将奖励完整的答案,无论是否在我的问题有资格获得赏金之前提供。

在任何 ECMAScript 内置函数上调用 setSuperclassOf 函数 class 不会影响构造函数的行为。

您的 HypotheticalArray 构造函数不应 - 绝不能 - 调用 super()。在规范中,你不应该只看 The Array Constructor section which gives a short overview, but also at the subsections §22.1.1.1 Array(), §22.1.1.2 Array(len) and §22.1.1.3 Array(...items) which give the detailed algorithms of what happens when you call Array (as either a function or constructor). They do look up the prototype of the (to be subclassable as usual - since ES6), but they do not look up the prototype of the Array function itself. Instead, they all do directly dispatch to the ArrayCreate algorithm,它只是创建一个对象,设置它的原型并安装奇异的 属性 语义。

String (which dispatches to the StringCreate algorithm when called as a constructor), the abstract TypedArray constructor (which just throws and explicitly states that "The TypedArray constructors do not perform a super call to it."), the concrete TypedArray constructors (which dispatch to the AllocateTypedArray and IntegerIndexedObjectCreate algorithms), and the Map and Set constructors (which both dispatch to the OrdinaryCreateFromConstructor and ObjectCreate 算法类似)。而且 afaik 它对于所有其他内置构造函数也是一样的,虽然我没有单独检查它们中的每一个,但在 ES8 中有太多了。

My understanding is that because Array.prototype itself is an Array exotic object, a compliant implementation is not required to internally call super() within the Array constructor in order to properly initialize the instance as an array exotic

不,这与它无关。一个对象不会因为它继承自一个奇异对象而变得奇异。一个对象是奇异的,因为它是专门创建的。 Array.prototype 的值实际上可以是任何东西,它与数组实例的创建无关——除此之外它将在 new Array 被调用时用作原型(与 new ArraySubclass).

关于 Object.setPrototypeOf(Array.prototype, …),请注意 Array.prototype 甚至不像 Object.prototype 那样的 ,所以是的,您可以这样做。