Подклассификация массива с помощью setPrototypeOf

Итак, я прочитал несколько сообщений в блоге, SO-темы и другие лекции о подклассификации Array в JavaScript. Общий взгляд на эту тему заключается в том, что нет никакого способа создать подкласс с некоторым недостатком.

Попробовав несколько вещей, я придумал это решение для себя:

// This is the constructor of the new class.
function CustomArray() {

    // The "use strict" statement changes the way the "magic" variable
    // "arguments" works and makes code generally safer.
    "use strict";

    // Create an actual array. This way the object will treat numeric
    // properties in the special way it is supposed to.
    var arr = [],
        i;

    // Overwrite the object prototype, so that CustomArray.prototype is
    // in the prototype chain and the object inherits its methods. This does
    // not break the special behaviour of arrays.
    Object.setPrototypeOf(arr, CustomArray.prototype);

    // Take all arguments and push them to the array.
    for (i = 0; i < arguments.length; i++) {
        arr.push(arguments[i]);
    }

    // Return the array with the modified prototype chain. This overwrites
    // the return value of the constructor so that CustomArray() really
    // returns the modified array and not "this".
    return arr;
}

// Make CustomArray inherit from Array.
CustomArray.prototype = Object.create(Array.prototype);

// Define a method for the CustomArray class.
CustomArray.prototype.last = function () {
    return this[this.length - 1];
};

var myArray = new CustomArray("A", "B", 3);
// [ "A", "B", 3 ]

myArray.length;
// 3

myArray.push("C");
// [ "A", "B", 3, "C" ]

myArray.length;
// 4

myArray.last();
// "C"

Мой вопрос: что-то не так с этим кодом? Мне трудно поверить, что я придумал "одно решение" после того, как многие люди искали меня передо мной.

Ответ 1

В статье обсуждается, как создать массив "подкласс". То есть мы хотим создать объект с Array.prototype в цепочке прототипов, но с непосредственным родителем-прототипом, который не является Array.prototype (т.е. Родитель-прототип может предоставить дополнительные методы за пределами прототипа массива).

В этой статье говорится, что фундаментальная трудность создания массива "подкласс" заключается в том, что массивы получают свое поведение от обоих

  • их прототип, и
  • просто являются экземплярами массива.

Если массивы унаследовали все их поведение от Array.prototype, наша работа была бы очень быстрой. Мы просто создали бы объект, чья цепочка прототипов включает Array.prototype. Этот объект станет идеальным прототипом для наших экземпляров массива-подкласса.

Однако массивы имеют специальное автоматическое поведение, уникальное для экземпляров массива и не унаследованное от прототипа. (В частности, я имею в виду поведение вокруг свойства length, которое автоматически изменяется при изменении массива и наоборот). Это поведение, предоставляемое каждому экземпляру массива, когда оно создается конструктором Array, и нет способа чтобы точно имитировать их в ECMAScript 5. Поэтому экземпляр вашего подкласса массива должен быть первоначально создан конструктором Array. Это не подлежит обсуждению, если мы хотим, чтобы соответствующие поведения length.

Это требование противоречит нашему другому требованию о том, что экземпляр должен иметь прототип, который не является Array.prototype. (Мы не хотим добавлять методы к Array.prototype, мы хотим добавить методы к объекту, который использует Array.prototype в качестве своего собственного прототипа.) В ECMAScript 5 любой объект, созданный с использованием конструктора Array, должен иметь прототип родительский элемент Array.prototype. Спецификация ECMAScript 5 не предоставляет механизма для изменения прототипа объекта после его создания.

В отличие от этого ECMAScript 6 предоставляет такой механизм. Ваш подход очень похож на подход, основанный на __proto__, описанный в статье, в разделе " Wrappers. Вставка цепи прототипа.," except вы используете ECMAScript 6 Object.setPrototypeOf вместо __proto__.

Ваше решение правильно удовлетворяет всем следующим требованиям:

  • Каждый экземпляр фактически представляет собой массив (т.е. был сконструирован конструктором Array). Это гарантирует правильность внутреннего свойства [[Class]], а length ведет себя правильно.
  • Каждый экземпляр имеет непосредственный прототип, который не является Array.prototype, но все еще содержит Array.prototype в цепочке прототипов.

Эти требования ранее не были удовлетворены в ES5, но ES6 делает его довольно простым. В ES5 у вас может быть экземпляр массива, который не удовлетворяет требованию №2 или простому объекту, который не удовлетворяет требованию № 1.

Ответ 2

Собственно, подклассы массива возможны, даже не касаясь Object.setPrototypeOf() или __proto__, используя таинственный метод Array.of(). Array.of() имеет возможность переключать конструкторную функцию, которую он использует для построения массива. Поскольку он обычно привязан к объекту Array, он создает нормальные массивы, но как только он привязан к другому объекту, который может использоваться как конструктор (функция a.k.a), он использует этот объект в качестве конструктора. Давайте сделаем некоторый подкласс класса с Array.of()

function SubArr(){}
SubArr.prototype = Object.create(Array.prototype);
SubArr.prototype.last = function(){return this[this.length-1]};

var what = Array.of.call(SubArr, 1, 2, 3, 4, "this is last");
console.log(JSON.stringify(what,null,2));
console.log(what.last());
console.log(what.map(e => e));
console.log(what instanceof Array);
console.log(Array.isArray(what));
what.unshift("this is first");
console.log(JSON.stringify(what,null,2));