0

So I read some blog posts, SO threads and other lectures about subclassing Array in JavaScript. The general view on the topic is that there is no way to create a subclass with some kind of downside.

While trying out a few things I came up with this solution for myself:

// 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's 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"

My question is: Is there anything wrong with this code? I find it hard to believe that I came up with "the one solution" after so many people searched before me.

Butt4cak3
  • 2,389
  • 15
  • 15
  • 2
    This isn't an answer to your question, but note that the article you link to is entitled "How *ECMAScript 5* still does not allow to subclass array" and your solution uses an ECMAScript **6** function. – apsillers Jan 16 '15 at 13:59
  • @apsillers You're right. I didn't even think about that. But the article still seems to apply. The problems described are still the same in ECMAScript 6. – Butt4cak3 Jan 16 '15 at 14:02
  • @apsillers This is kind of embarrassing. That line somehow got lost when I built the simplified example for this question. I edited the question to include the line of code. – Butt4cak3 Jan 16 '15 at 15:56

2 Answers2

2

The article discusses how to create an array "subclass". That is, we want to create an object that has Array.prototype in its prototype chain, but with an immediate prototype parent that is not Array.prototype (i.e., so that the prototype parent can provide additional methods beyond the array prototype).

That article says that a fundamental difficulty in creating an array "subclass" is that arrays get their behavior from both

  1. their prototype, and
  2. simply being array instances.

If arrays inherited all their behavior from Array.prototype, our work would be very quick. We would simply create an object whose prototype chain includes Array.prototype. That object would make an ideal prototype for our array-subclass instances.

However, arrays have special automatic behaviors that are unique to array instances, and are not inherited from the prototype. (In particular, I mean the behaviors around the length property automatically changing when the array changes, and vice versa.) These are behaviors supplied to each array instance when it is created by the Array constructor, and there is no way to mimic them faithfully in ECMAScript 5. Therefore, the instance of your array subclass must be originally created by the Array constructor. This is non-negotiable, if we want the appropriate length behaviors.

This requirement conflicts with our other requirement that the instance must have a prototype that is not Array.prototype. (We don't want to add methods to Array.prototype; we want to add methods to an object that uses Array.prototype as its own prototype .) In ECMAScript 5, any object created using the Array constructor must have the prototype parent of Array.prototype. The ECMAScript 5 spec provides no mechanism to change an object's prototype after it is created.

By contrast, ECMAScript 6 does provide such a mechanism. Your approach is quite similar to the __proto__-based approach described in the article, under the section "Wrappers. Prototype chain injection.," except you use ECMAScript 6's Object.setPrototypeOf instead of __proto__.

Your solution correctly satisfies all of the following requirements:

  1. Each instance actually is an array (i.e., has been constructed by the Array constructor). This ensures that the [[Class]] internal property is correct, and length behaves correctly.
  2. Each instance has an immediate prototype that is not Array.prototype, but still includes Array.prototype in its prototype chain.

These requirements were previously impossible to satisfy in ES5, but ES6 makes it fairly easy. In ES5, you could have an array instance that fails to satisfy requirement #2, or a plain object that fails to satisfy requirement #1.

apsillers
  • 112,806
  • 17
  • 235
  • 239
  • 1
    Nice, in-depth answer, although it doesn't really answer my original question. I am familiar with the problems you run into when trying to subclass `Array`, otherwise I *probably* wouldn't have been able to come up with a solution. I asked for reasons **not** to use my code or in other words: "Is this a good solution?". Although I'm starting to have the feeling that SO is the wrong platform for this type of question... – Butt4cak3 Jan 16 '15 at 16:04
  • @Butt4cak3 I've added a little bit at the end demonstrating that you're fulfilled the two requirements. There does not appear to be anything wrong with your code. Of course, I can't *prove* the absence of problems; perhaps someone else will identify one. – apsillers Jan 16 '15 at 16:40
  • Thanks. I will still mark your answer as the correct one since it's very informative. – Butt4cak3 Jan 16 '15 at 22:08
  • A subclass made by this solution requires the same mechanism to its own subclasses. One small problem ;) – Kagami Sascha Rosylight Mar 25 '15 at 13:38
1

Actually array subclassing is possible without even touching Object.setPrototypeOf() or __proto__ by utilizing the mysterious Array.of() method. Array.of() has the ability to switch the constructor function that it uses to construct an array. Since normally it's bound to the Array object it produces normal arrays however once it's bound to another object which can be used as a constructor (a.k.a function) it uses that object as a constructor. Lets make some array sub-classing with 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));

So as you see array sub-classing is pretty simple task when done with Array.of() You can find it's specs here. The interesting part is;

NOTE 2 The of function is an intentionally generic factory method; it does not require that its this value be the Array constructor. Therefore it can be transferred to or inherited by other constructors that may be called with a single numeric argument.

Redu
  • 25,060
  • 6
  • 56
  • 76