Позволяет ли спецификация ECMAScript быть суперклассом Array?

Я ищу какие-либо признаки того, будет ли суперклассирование встроенного типа работать в соответствии со спецификацией. То есть, учитывая любую гипотетическую совместимую реализацию 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 Конструктор массива

Конструктор массива:

  • создает и инициализирует новый экзотический объект Array при вызове в качестве конструктора.
  • предназначен для подклассов. Его можно использовать как значение предложения extends определения класса. Конструкторы подклассов, которые намереваются наследовать экзотическое поведение Array, должны включать супервызов конструктора Array для инициализации экземпляров подкласса, которые являются экзотическими объектами Array.

§22.1.3 Свойства массива Объект-прототип

У объекта-прототипа Array есть внутренний слот [[Prototype]], значением которого является встроенный объект %ObjectPrototype%.

Объект-прототип Array указан как экзотический объект Array, чтобы обеспечить совместимость с кодом ECMAScript, созданным до спецификации ECMAScript 2015.

(выделение добавлено)

Насколько я понимаю, совместимая реализация не обязана внутренне вызывать super() в конструкторе Array, чтобы правильно инициализировать экземпляр как экзотический массив, а также не требует, чтобы Object был прямым суперклассом Array ( хотя моя первая цитата из §22.1.3 определенно подразумевает именно это).

Мой вопрос: работает ли первый приведенный выше фрагмент в соответствии со спецификацией или он работает только потому, что существующие в настоящее время реализации позволяют это делать? т. е. является ли реализация первого HypotheticalArray несовместимой?

И для получения полной награды я также хотел бы применить этот вопрос к String, Set, Map и TypedArray (под которыми я подразумеваю Object.getPrototypeOf(Uint8Array.prototype).constructor).

Я награжу 500 бонусными баллами за первый ответ, который строго отвечает на мои вопросы о практике суперклассирования вышеуказанных встроенных функций в ECMAScript 2015 и более поздних версиях (черновик, в котором был представлен Object.setPrototypeOf()).

Я не собираюсь поддерживать ECMAScript версии 5.1 и ниже, так как изменение встроенной цепочки наследования возможно только при доступе к __proto__, который не является частью какой-либо спецификации ECMAScript и, следовательно, зависит от реализации.

P.S. Я полностью осознаю причины, по которым подобные практики не приветствуются, поэтому я хотел бы определить, допускает ли спецификация суперклассирование без нарушения работы сети, как любит говорить TC39.


person Patrick Roberts    schedule 05.07.2018    source источник
comment
Вы спрашиваете, разрешено ли вам изменять цепочку прототипов массива (или других встроенных типов)?   -  person Barmar    schedule 05.07.2018
comment
@Barmar По сути, да, но только с ограничениями, которые я описал в своем предложении Соломенного человечка о суперклассируемом классе. И я хочу знать, гарантирует ли спецификация, что он будет вести себя без непреднамеренных побочных эффектов в любой данной реализации.   -  person Patrick Roberts    schedule 05.07.2018
comment
С какой стати вам изменить поведение существующих глобальных переменных таким образом? Даже если это технически работает, вы нарушите ожидания любого существующего кода на странице.   -  person loganfsmyth    schedule 05.07.2018
comment
@loganfsmyth Весь смысл этого вопроса в том, чтобы гарантировать, что в соответствии со спецификацией этого не произойдет. Если существующий код не ожидает, что Object.getPrototypeOf(Array.prototype) будет Object.prototype (для чего нет веских причин ожидать этого), никто не может разумно ожидать, что это нарушит какие-либо существующие основы кода. Есть разница между ожиданием наследования (которое я не нарушаю) и ожиданием прямого суперкласса (которое, как я утверждаю, здесь является необоснованным ожиданием).   -  person Patrick Roberts    schedule 05.07.2018
comment
Ладно, достаточно честно.   -  person loganfsmyth    schedule 05.07.2018
comment
Одна вещь, которую я заметил, заключается в том, что если вы class Test extends Object {}, результирующий класс требует вызова super() в constructor() независимо от того, определен ли constructor явно в определении или нет, потому что суперклассирование его с помощью class Verbose { constructor () { console.log('called super()') } } приведет к созданию журнала консоли в любое время. Test построен. Однако при объявлении class Test {} и суперклассировании его с помощью Verbose вызов super отсутствует. Гарантируется ли такое поведение спецификацией? Потому что это, кажется, напрямую связано с моим вопросом.   -  person Patrick Roberts    schedule 07.07.2018
comment
Вы уверены, что Object.setPrototypeOf(Array, Enumerable) позволяет конструкторам super указывать на перечисление?   -  person Jonas Wilms    schedule 07.07.2018
comment
@ДжонасВ. если вы посмотрите на мой ответ, я говорю, что (к счастью) конструктор Array никогда не вызывает super() в соответствии со спецификацией, но для класса, чей [[ConstructorKind]] является производным, я говорю да, это изменит то, на что ссылается super , поэтому производный класс не является суперклассом.   -  person Patrick Roberts    schedule 07.07.2018
comment
Я должен уточнить, что производный класс на самом деле является суперклассом, если вставленный суперкласс имеет неявный конструктор (который в соответствии со спецификацией определяется как constructor (...args) { super(...args) }) или он пересылает такие аргументы в дополнение к своей собственной специфичной для класса инициализации.   -  person Patrick Roberts    schedule 07.07.2018


Ответы (2)


Вызов вашей функции setSuperclassOf для любого встроенного класса ECMAScript не повлияет на поведение конструктора.

Ваш конструктор HypotheticalArray не должен — не должен — вызывать super(). В спецификации вы не должны просто смотреть на Раздел конструктора массивов, в котором дается краткий обзор, а также подразделы §22.1.1.1 Array(), §22.1.1.2 Array(len) и §22.1.1.3 Array(...items), которые содержат подробные алгоритмы того, что происходит, когда вы вызовите Array (как функцию или конструктор). Они ищут прототип newTarget (чтобы быть подклассом, как обычно - начиная с ES6), но они не ищут прототип самой функции Array. Вместо этого все они отправляются напрямую в ArrayCreate. алгоритм, который просто создает объект, настраивает его прототип и устанавливает семантику экзотических свойств.

Аналогично для String (который отправляет к алгоритму StringCreate при вызове в качестве конструктора), абстрактный TypedArray конструктор (который просто выдает и явно указывает, что "конструкторы TypedArray не выполняют к нему супервызов."), конкретные конструкторы TypedArray (которые отправляются в AllocateTypedArray и IntegerIndexedObjectCreate) и Map и Set конструкторы (которые оба отправляют в OrdinaryCreateFromConstructor< /a> и алгоритмы ObjectCreate ). И, на самом деле, то же самое и для всех других встроенных конструкторов, хотя я не проверял каждый из них по отдельности, их слишком много по сравнению с ES8.

Насколько я понимаю, поскольку Array.prototype сам по себе является экзотическим объектом Array, совместимая реализация не требуется для внутреннего вызова super() в конструкторе Array, чтобы правильно инициализировать экземпляр как экзотический массив.

Нет, это не имеет к этому никакого отношения. Объект не становится экзотическим, потому что он наследуется от экзотического объекта. Объект экзотичен, потому что он был специально создан таким. Значение Array.prototype может быть любым, оно не имеет значения для создания экземпляров массива, кроме того, что оно будет использовано в качестве прототипа при вызове new Array (в отличие от new ArraySubclass).

Что касается Object.setPrototypeOf(Array.prototype, …), обратите внимание, что Array.prototype даже не является неизменяемым экзотическим объектом-прототипом, таким как Object.prototype, так что да, вам разрешено это делать.

person Bergi    schedule 07.07.2018
comment
@PatrickRoberts ArrayCreate не вызывает Object и не разрешает суперклассы. В этом отношении он вообще не является общим - он является общим только для объекта-прототипа подкласса, от которого он наследуется. - person Bergi; 08.07.2018
comment
@PatrickRoberts Да, я говорю, что вызов super() в HypotheticalArray незаконен (не соответствует спецификации Array), см. второе предложение моего ответа. И нет, суперклассирование невозможно (не поддерживается) ни одним встроенным классом, вызов setSuperclassOf вообще не повлияет на поведение их конструктора. - person Bergi; 08.07.2018
comment
Вы, кажется, неправильно понимаете, что я говорю. Я называю класс надклассовым, если вызов setSuperclassOf не влияет на их поведение (кроме предоставления новых методов экземплярам класса) - person Patrick Roberts; 08.07.2018
comment
И, оглядываясь назад на мой предыдущий комментарий, я думаю, что общий выбор слова, возможно, был неудачным с моей стороны. Я думаю, что мы полностью согласны, кроме определения superclassable. - person Patrick Roberts; 08.07.2018
comment
@PatrickRoberts О, да, я понял, что суперкласс поддерживает динамический суперкласс через setSuperclassOf. Удалил термин из моего ответа сейчас. - person Bergi; 09.07.2018
comment
На данный момент я думаю, что ваш ответ подходит для награды, справедливой и справедливой. Я подумывал подождать еще несколько дней, но, просмотрев это, нашел почти всю информацию, которую искал. - person Patrick Roberts; 09.07.2018

На основании гарантии, изложенной в §9.3.3 CreateBuiltinFunction ( шаги< /em>, internalSlotsList [ , область [ , прототип ] ] ) и шаги в §22.1.1 Конструктор массива, ни один возможный вызов Array(…) или new Array(…) не вызовет Конструктор объекта или конструктор разрешенного суперкласса Array во время вызова и, следовательно, «суперклассирующий» Array гарантированно будет вести себя правильно в любой соответствующей реализации ECMAScript 2018.

Из-за открытия §9.3.3 я подозреваю, что такой же вывод будет сделан для остальных классов в текущей спецификации, хотя требуется гораздо больше исследований, чтобы определить, является ли это точным и гарантированным до ECMAScript 2015.

Это не полный ответ, поэтому я его не приму. Награда по-прежнему будет начисляться за полный ответ, независимо от того, был ли он предоставлен до того, как мой вопрос соответствует критериям для награды.

person Patrick Roberts    schedule 06.07.2018