Subclassing Javascript Arrays. TypeError: Array.prototype.toString is not generic

Viewed 20508

Is it possible to subclass and inherit from javascript Arrays?

I'd like to have my own custom Array object that has all the features of an Array, but contains additional properties. I'd use myobj instanceof CustomArray to perform specific operations if the instance is my CustomArray.

After attempting to subclass and running into some problems, I found this Dean Edwards article that indicates doing this with Array objects doesn't work right. It turns out Internet Explorer doesn't handle it properly. But I'm finding other issues as well (only tested in Chrome so far).

Here's some sample code:

/** 
 *  Inherit the prototype methods from one constructor into another 
 *  Borrowed from Google Closure Library 
 */
function inherits(childCtor, parentCtor) {
    function tempCtor() {};
    tempCtor.prototype = parentCtor.prototype;
    childCtor.superClass_ = parentCtor.prototype;
    childCtor.prototype = new tempCtor();
    childCtor.prototype.constructor = childCtor;
},

// Custom class that extends Array class
function CustomArray() {
    Array.apply(this, arguments);
}
inherits(CustomArray,Array);

array = new Array(1,2,3);
custom = new CustomArray(1,2,3);

Entering the following in Chrome's console gives this output:

> custom
[]
> array
[1, 2, 3]
> custom.toString()
TypeError: Array.prototype.toString is not generic
> array.toString()
"1,2,3"
> custom.slice(1)
[]
> array.slice(1)
[2, 3]
> custom.push(1)
1
> custom.toString()
TypeError: Array.prototype.toString is not generic
> custom
[1]

Obviously, the objects don't behave the same. Should I give up on this approach, or is there some way to accomplish my goal of myobj instanceof CustomArray?

7 Answers

Here's a full example that should work on ie9 and greater. For <=ie8 you'd have to implement alternatives to Array.from, Array.isArray, etc. This example:

  • Puts the Array subclass in its own closure (or Namespace) to avoid conflicts and namespace pollution.
  • Inherits all prototypes and properties from the native Array class.
  • Shows how to define additional properties and prototype methods.

If you can use ES6, you should use the class SubArray extends Array method laggingreflex posted.

Here is the essentials to subclass and inherit from Arrays. Below this excerpt is the full example.

///Collections functions as a namespace.     
///_NativeArray to prevent naming conflicts.  All references to Array in this closure are to the Array function declared inside.     
var Collections = (function (_NativeArray) {
    //__proto__ is deprecated but Object.xxxPrototypeOf isn't as widely supported. '
    var setProtoOf = (Object.setPrototypeOf || function (ob, proto) { ob.__proto__ = proto; return ob; });
    var getProtoOf = (Object.getPrototypeOf || function (ob) { return ob.__proto__; });        

    function Array() {          
        var arr = new (Function.prototype.bind.apply(_NativeArray, [null].concat([].slice.call(arguments))))();           
        setProtoOf(arr, getProtoOf(this));     
        return arr;
    }

    Array.prototype = Object.create(_NativeArray.prototype, { constructor: { value: Array } });
    Array.from = _NativeArray.from; 
    Array.of = _NativeArray.of; 
    Array.isArray = _NativeArray.isArray;

    return { //Methods to expose externally. 
        Array: Array
    };
})(Array);

Full example:

///Collections functions as a namespace.     
///_NativeArray to prevent naming conflicts.  All references to Array in this closure are to the Array function declared inside.     
var Collections = (function (_NativeArray) {
    //__proto__ is deprecated but Object.xxxPrototypeOf isn't as widely supported. '
    var setProtoOf = (Object.setPrototypeOf || function (ob, proto) { ob.__proto__ = proto; return ob; });
    var getProtoOf = (Object.getPrototypeOf || function (ob) { return ob.__proto__; });        

    function Array() {          
        var arr = new (Function.prototype.bind.apply(_NativeArray, [null].concat([].slice.call(arguments))))();           
        setProtoOf(arr, getProtoOf(this));//For any prototypes defined on this subclass such as 'last'            
        return arr;
    }

    //Restores inherited prototypes of 'arr' that were wiped out by 'setProtoOf(arr, getProtoOf(this))' as well as add static functions.      
    Array.prototype = Object.create(_NativeArray.prototype, { constructor: { value: Array } });
    Array.from = _NativeArray.from; 
    Array.of = _NativeArray.of; 
    Array.isArray = _NativeArray.isArray;

    //Add some convenient properties.  
    Object.defineProperty(Array.prototype, "count", { get: function () { return this.length - 1; } });
    Object.defineProperty(Array.prototype, "last", { get: function () { return this[this.count]; }, set: function (value) { return this[this.count] = value; } });

    //Add some convenient Methods.          
    Array.prototype.insert = function (idx) {
        this.splice.apply(this, [idx, 0].concat(Array.prototype.slice.call(arguments, 1)));
        return this;
    };
    Array.prototype.insertArr = function (idx) {
        idx = Math.min(idx, this.length);
        arguments.length > 1 && this.splice.apply(this, [idx, 0].concat([].pop.call(arguments))) && this.insert.apply(this, arguments);
        return this;
    };
    Array.prototype.removeAt = function (idx) {
        var args = Array.from(arguments);
        for (var i = 0; i < args.length; i++) { this.splice(+args[i], 1); }
        return this;
    };
    Array.prototype.remove = function (items) {
        var args = Array.from(arguments);
        for (var i = 0; i < args.length; i++) {
            var idx = this.indexOf(args[i]);
            while (idx !== -1) {
                this.splice(idx, 1);
                idx = this.indexOf(args[i]);
            }
        }
        return this;
    };

    return { //Methods to expose externally. 
        Array: Array
    };
})(Array);

Here are some usage examples and tests.

var colarr = new Collections.Array("foo", "bar", "baz", "lorem", "ipsum", "lol", "cat");
var colfrom = Collections.Array.from(colarr.reverse().concat(["yo", "bro", "dog", "rofl", "heyyyy", "pepe"]));
var colmoded = Collections.Array.from(colfrom).insertArr(0, ["tryin", "it", "out"]).insert(0, "Just").insert(4, "seems", 2, "work.").remove('cat','baz','ipsum','lorem','bar','foo');  

colmoded; //["Just", "tryin", "it", "out", "seems", 2, "work.", "lol", "yo", "bro", "dog", "rofl", "heyyyy", "pepe"]

colmoded instanceof Array; //true

ES6 minimal runnable example with custom constructor

If you also want to override the constructor, then some extra care is needed because some of the methods will need the old constructor.

Using the techniques mentioned at: How can I extend the Array class and keep its implementations we can reach:

#!/usr/bin/env node

const assert = require('assert');

class MyArray extends Array {
  constructor(nodes, myint) {
    super(...nodes);
    this.myint = myint;
  }

  static get [Symbol.species]() {
    return Object.assign(function (...items) {
      return new MyArray(new Array(...items))
    }, MyArray);
  }

  inc() { return this.myint + 1; }
}

const my_array = new MyArray([2, 3, 5], 9);
assert(my_array[0] === 2);
assert(my_array[1] === 3);
assert(my_array[2] === 5);

assert(my_array.myint === 9);

assert(my_array.inc() === 10);

assert(my_array.toString() === '2,3,5');

my_slice = my_array.slice(1, 2);
assert(my_slice[0] === 3);
assert(my_slice.constructor === MyArray);

Getting the index notation [] without Arrray has been asked at: Implement Array-like behavior in JavaScript without using Array

Tested in Node.js v10.15.1.

Related