【问题标题】:Does the ECMAScript specification allow Array to be "superclassable"?ECMAScript 规范是否允许 Array 成为“超类”?
【发布时间】:2018-12-14 07:26:22
【问题描述】:

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

“超类”,我正在创造的一个术语,指的是一个类,其通过构造它返回的对象,或者如果适用的话将它作为函数调用,将使用相同的内部槽(除了[[Prototype]]),不管它的直接超类是什么,只要类构造函数的初始[[Prototype]]和类原型在重新赋值后还在各自的继承链中。因此,为了成为“超类”,类在创建期间不得调用super()

当“超类化”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 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 “超类化”,如果需要一个一致的实现不是来调用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

数组构造函数:

  • 在作为构造函数调用时创建并初始化一个新的 Array 外来对象
  • 设计为可子类化。它可以用作类定义的扩展子句的值。 打算继承奇异 Array 行为的子类构造函数必须包含对 Array 构造函数的超级调用,以初始化作为 Array 奇异对象的子类实例。

§22.1.3 Properties of the Array Prototype Object

Array 原型对象有一个 [[Prototype]] 内部槽,其值为内部对象 %ObjectPrototype%。

Array 原型对象被指定为 Array 外来对象,以确保与在 ECMAScript 2015 规范之前创建的 ECMAScript 代码兼容。

(强调)

我的理解是,不需要Array 构造函数中内部调用 super() 以将实例正确初始化为外来数组,也不需要 @987654335 @ 成为 Array 的直接超类(尽管我第一次引用 §22.1.3 似乎暗示了这一点)。

我的问题是,上面的第一个 sn-p 是否根据规范工作,或者它是否仅因为当前现有的实现允许它工作?即第一个HypotheticalArray的实现是否不符合?

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

我将奖励 500 赏金积分,严格解决了我关于在 ECMAScript 2015 及更高版本中“超类化”上述内置函数(引入Object.setPrototypeOf() 的草案)的实践的问题)。

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

附注我完全了解不鼓励这种做法的原因,这就是为什么我想确定规范是否允许“超类化”而不“破坏网络”,正如 TC39 喜欢说的那样。

【问题讨论】:

  • 您是在问是否允许您更改数组(或其他内置类型)的原型链?
  • @Barmar 基本上是的,但仅限于我在我的稻草人提案中描述的“超类”类的限制。而且我想知道规范是否保证它在任何给定的实现中都不会产生意外的副作用。
  • 你到底为什么要像这样修改现有全局变量的行为?即使它在技术上有效,你也会打破页面中任何现有代码的期望。
  • @loganfsmyth 这个问题的重点是确保根据规范,这种情况不会发生。除非现有代码期望 Object.getPrototypeOf(Array.prototype)Object.prototype(这是没有充分的理由期望),否则没有人会合理地期望这会破坏任何现有的代码库。期望继承(我没有破坏)和期望直接超类(我在这里断言这是不合理的期望)之间存在差异。
  • 好吧,够公平的。

标签: javascript arrays ecmascript-6 language-lawyer prototypal-inheritance


【解决方案1】:

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

您的 HypotheticalArray 构造函数不应 - 绝不能 - 调用 super()。在规范中,您不应该只查看提供简短概述的The Array Constructor section,还应查看§22.1.1.1 Array()§22.1.1.2 Array(len)§22.1.1.3 Array(...items) 小节,它们提供了调用Array 时发生的详细算法(作为函数或构造函数)。他们确实会查找 newTarget 的原型(像往常一样可以子类化 - 从 ES6 开始),但他们不会查找 Array 函数本身的原型。相反,它们都直接发送到ArrayCreate algorithm,它只是创建一个对象,设置它的原型并安装奇异的属性语义。

对于String(作为构造函数调用时会分派到StringCreate algorithm)和abstract TypedArray constructor(只是抛出并明确声明“TypedArray 构造函数不会对it.")、concrete TypedArray constructors(分派给AllocateTypedArrayIntegerIndexedObjectCreate算法)和MapSet构造函数(都分派给OrdinaryCreateFromConstructor和@987654336 @算法)。而且 afaik 对于所有其他内置构造函数也是一样的,虽然我没有单独检查它们,但从 ES8 开始太多了。

我的理解是,因为Array.prototype 本身是一个数组奇特对象,所以不需要在Array 构造函数中内部调用super() 以将实例正确初始化为奇特数组

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

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

【讨论】:

  • @PatrickRoberts ArrayCreate 也不调用 Object 或解析任何超类。在这方面它根本不是通用的 - 它只是在要继承的子类原型对象上通用。
  • @PatrickRoberts 是的,我是说在HypotheticalArray 中调用super() 是非法的(不符合Array 的规范),请参阅我回答的第二句。不,任何内置类都不可能(不支持)“超类化”,调用 setSuperclassOf 根本不会影响它们的构造函数行为。
  • 您似乎误解了我的意思。如果调用setSuperclassOf 不影响它们的行为(除了为类的实例提供新方法),我将调用一个“超类”类
  • 回顾我之前的评论,我认为“通用”可能对我来说是一个糟糕的词选择。除了“超类”的定义之外,我认为我们完全一致。
  • @PatrickRoberts 哦,对了,我将“超类”理解为“通过setSuperclassOf 支持动态超类”。现在从我的回答中删除了这个词。
【解决方案2】:

基于§9.3.3 CreateBuiltinFunction ( steps, internalSlotsList [ , realm [ , prototype ] ] ) 中概述的保证和§22.1.1 The Array Constructor 中的步骤,Array(…)new Array(…) 的任何可能调用都不会在调用时调用 Object 构造函数或 Array 的已解析超类的构造函数,因此“超类化” Array 保证在任何符合 ECMAScript 2018 的实现中都能正常运行。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-03-29
    • 2011-08-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-02
    相关资源
    最近更新 更多