kjo
kjo

Reputation: 35321

Trying to implement an easily-"subclassable" Array subclass

I'm trying to subclass Array, following the ideas and recommendations given in this article.

One important goal of this subclass of Array, which here I'm calling ArrayBase, is that it be itself more straightforward to subclass than Array itself. I'm finding that achieving this goal is surprisingly difficult. (But it may look this way to me because I'm a JavaScript n00b!)

Below is an implementation of ArrayBase that is based on the ideas presented towards the end of the article cited earlier, with some enhancements of my own. I've also included an implementation of ArrayBase.slice, since it illustrates one problem with the scheme1.

function ArrayBase () {
  var arr = Array.prototype.constructor.apply(null, arguments);
  var ctor = arr.constructor = arguments.callee;
  arr.__proto__ = ctor.prototype;
  return arr;
}

ArrayBase.prototype = new Array;
ArrayBase.prototype.slice = function () {
  var ctor = this.constructor;
  return ctor.apply(null,
                    Array.prototype.slice.apply(this, arguments));
}

var a0 = new ArrayBase(0, 1, 2, 3);
var a1 = a0.slice(2);                 // [2, 3]
console.log(a1 instanceof ArrayBase); // true
console.log(a1 instanceof Array);     // true

So far, so good. The problem happens when I now try to subclass ArrayBase. I find that the only way to do this requires basically replicating the entire ArrayBase constructor (the only difference, very slight, happens in the first line). As inheritance goes, this is pitiful...

function MyArray () {
  var arr = ArrayBase.apply(this, arguments);
  var ctor = arr.constructor = arguments.callee;
  arr.__proto__ = ctor.prototype;
  return arr;
}
MyArray.prototype = new ArrayBase;

// behavior of MyArray

var a2 = new MyArray(1, 2, 3, 0);
var a3 = a2.slice(1);                 // [2, 3, 0]
console.log(a3 instanceof MyArray);   // true
console.log(a3 instanceof ArrayBase); // true
console.log(a3 instanceof Array);     // true
console.log(a3.join(':'));            // "2:3:0"
a3[5] = 1;
console.log(a3.length);               // 6
a3.length = 2;
console.log(a3.toString())            // "2,3"

My questions:

How could the duplication that exists between the ArrayBase and MyArray constructors, be eliminated, while still preserving the behavior illustrated by the lines after the // behavior of MyArr line? Would the scheme work also at the time of subclassing MyArray?

(I'm aware of the arguments against building tall towers of inheritance, but whether they are good design or not, I want them to be at least soundly implemented.)


1If inheritance from Array were as I think it should be, it would not be necessary to implement ArrayBase.slice, but unfortunately the slice method that ArrayBase inherits from Array does not show the elementary OOP courtesy of returning an object of the same class as that of this.

Upvotes: 2

Views: 205

Answers (1)

Bergi
Bergi

Reputation: 664930

Before answering your question, some comments on the code :-)

var arr = Array.prototype.constructor.apply(null, arguments);

Since Array.prototype.constructor === Array, don't duplicate this.

var ctor = arr.constructor = …

There's no reason to create a property here. If the constructor property is needed for anything, it should be inherited from that constructor's .prototype object.

arguments.callee;

Don't use deprecated arguments.callee! You know that it points to ArrayBase.

arr.__proto__ = ctor.prototype;

You probably know that __proto__ is nonstandard (and does especially not work in IE), but we need it here for the prototype injection technique. Still, don't forget this fact!

ArrayBase.prototype = new Array;

Do not use new for setting up inheritance! You don't want to invoke an initialiser (i.e. "constructor") here. Use Object.create instead.

Now, back to your question:

How can the duplication that exists between my ArrayBase and MyArray constructors be eliminated?

Actually you have used that concept already. Your ArrayBase.prototype.slice implementation works with every subclass - constructing instances of this.constructor again. You can use the same method for the ArrayBase constructor:

function ArrayBase() {
  var arr = Array.apply(null, arguments);
  var ctor = this.constructor;
  arr.__proto__ = ctor.prototype;
  return arr;
}
/* irrelevant for the answer, but helpful:
ArrayBase.prototype = Object.create(Array.prototype, {
  constructor: {value: ArrayBase}
});
Object.keys(Array.prototype).forEach(function(k) {
  if (typeof Array.prototype[k] != "function") return;
  ArrayBase.prototype[k] = function() {
    var arr = Array.prototype[k].apply(this, arguments);
    if (Array.isArray(arr))
      arr.__proto__ = Object.getPrototypeOf(this);
    return arr;
  };
});
*/
function MyArray() {
  return ArrayBase.apply(this, arguments);
}
MyArray.prototype = Object.create(ArrayBase.prototype, {
  constructor: {value: MyArray}
});

Upvotes: 2

Related Questions