38

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?

Tauren
  • 26,795
  • 42
  • 131
  • 167
  • is creating a wrapper an acceptable solution? The problem in the above is that `Array.apply` is returning a new object, ignoring the `this` context. – Anurag Jul 16 '10 at 03:19
  • @Anurag: By wrapper, do you mean doing something like this?http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/#wrappers_prototype_chain_injection Or did you have something else in mind? – Tauren Jul 16 '10 at 06:20

7 Answers7

36

Juriy Zaytsev (@kangax) just today released a really good article on the subject.

He explores various alternatives like the Dean Edwards iframe borrowing technique, direct object extension, prototype extension and the usage of ECMAScript 5 accessor properties.

At the end there is no perfect implementation, each one has its own benefits and drawbacks.

Definitely a really good read:

Community
  • 1
  • 1
Christian C. Salvadó
  • 807,428
  • 183
  • 922
  • 838
24

ES6

class SubArray extends Array {
    last() {
        return this[this.length - 1];
    }
}
var sub = new SubArray(1, 2, 3);
sub // [1, 2, 3]
sub instanceof SubArray; // true
sub instanceof Array; // true

Original Answer: (Not recommended, may cause performance issues)

Copy-pasting from article mentioned in the accepted answer for more visibility

Using __proto__

function SubArray() {
  var arr = [ ];
  arr.push.apply(arr, arguments);
  arr.__proto__ = SubArray.prototype;
  return arr;
}
SubArray.prototype = new Array;

Now you can add your methods to SubArray

SubArray.prototype.last = function() {
  return this[this.length - 1];
};

Initialize like normal Arrays

var sub = new SubArray(1, 2, 3);

Behaves like normal Arrays

sub instanceof SubArray; // true
sub instanceof Array; // true
Community
  • 1
  • 1
laggingreflex
  • 32,948
  • 35
  • 141
  • 196
  • This worked perfectly and makes sense form a beginners stand point. – antihero989 Nov 05 '15 at 21:36
  • but does Array.isArray return true? (i checked, and it does) – Sam Jan 16 '16 at 04:23
  • nice solution, I'd just modify one part. Instead of defining the prototype outside, you can do it inside the constructor with `this.prototype = newArray` Not sure if its bad practice though – yosefrow Sep 25 '16 at 22:43
  • @yosefrow `this` inside a function is actually the object on which the function was called (with `new` it would just be an empty object) and objects don't have `prototype` property, their `constructor`s do. ([Checkout difference between \_\_proto\_\_ and prototype](http://google.com/search?q=difference+__proto__+prototype)). Also since we're returning a completely self-constructed object (`arr = []`) from that (`SubArray`) function, modifying `this` wouldn't matter. – laggingreflex Sep 26 '16 at 01:22
  • Ok, thanks for explaining. I assumed that `this.prototype` referred to the prototype of any object that was returned. But now I see what you mean that since `this` is not being returned at all, its irrelevant what you do to `this` – yosefrow Sep 26 '16 at 06:28
1

Checkout this. It works as it should in all browsers which support '__proto__'.

var getPrototypeOf = Object.getPrototypeOf || function(o){
    return o.__proto__;
};
var setPrototypeOf = Object.setPrototypeOf || function(o, p){
    o.__proto__ = p;
    return o;
};

var CustomArray = function CustomArray() {
    var array;
    var isNew = this instanceof CustomArray;
    var proto = isNew ? getPrototypeOf(this) : CustomArray.prototype;
    switch ( arguments.length ) {
        case 0: array = []; break;
        case 1: array = isNew ? new Array(arguments[0]) : Array(arguments[0]); break;
        case 2: array = [arguments[0], arguments[1]]; break;
        case 3: array = [arguments[0], arguments[1], arguments[2]]; break;
        default: array = new (Array.bind.apply(Array, [null].concat([].slice.call(arguments))));
    }
    return setPrototypeOf(array, proto);
};

CustomArray.prototype = Object.create(Array.prototype, { constructor: { value: CustomArray } });
CustomArray.prototype.append = function(var_args) {
    var_args = this.concat.apply([], arguments);        
    this.push.apply(this, var_args);

    return this;
};
CustomArray.prototype.prepend = function(var_args) {
    var_args = this.concat.apply([], arguments);
    this.unshift.apply(this, var_args);

    return this;
};
["concat", "reverse", "slice", "splice", "sort", "filter", "map"].forEach(function(name) {
    var _Array_func = this[name];
    CustomArray.prototype[name] = function() {
        var result = _Array_func.apply(this, arguments);
        return setPrototypeOf(result, getPrototypeOf(this));
    }
}, Array.prototype);

var array = new CustomArray(1, 2, 3);
console.log(array.length, array[2]);//3, 3
array.length = 2;
console.log(array.length, array[2]);//2, undefined
array[9] = 'qwe';
console.log(array.length, array[9]);//10, 'qwe'
console.log(array+"", array instanceof Array, array instanceof CustomArray);//'1,2,,,,,,,,qwe', true, true

array.append(4);
console.log(array.join(""), array.length);//'12qwe4', 11
termi
  • 946
  • 9
  • 8
  • jsperf for this CustomArray here: 1. [write/read by index](http://jsperf.com/custom-array-vs-array/2) 2. [for, forEach, map](http://jsperf.com/sdngjkn/12) – termi Aug 01 '14 at 13:50
1

I've tried to do this sort of thing before; generally, it just doesn't happen. You can probably fake it, though, by applying Array.prototype methods internally. This CustomArray class, though only tested in Chrome, implements both the standard push and custom method last. (Somehow this methodology never actually occurred to me at the time xD)

function CustomArray() {
    this.push = function () {
        Array.prototype.push.apply(this, arguments);
    }
    this.last = function () {
        return this[this.length - 1];
    }
    this.push.apply(this, arguments); // implement "new CustomArray(1,2,3)"
}
a = new CustomArray(1,2,3);
alert(a.last()); // 3
a.push(4);
alert(a.last()); // 4

Any Array method you intended to pull into your custom implementation would have to be implemented manually, though you could probably just be clever and use loops, since what happens inside our custom push is pretty generic.

Matchu
  • 83,922
  • 18
  • 153
  • 160
  • Thanks, but this solution doesn't really create an object that performs like an array. You can add the methods to it, like you did with `push`, but it doesn't handle direct index manipulation. For instance, doing `a[5]=6` won't change the length like it would in a real Array. The article linked in @CMS answer goes over all the possible solutions and points out the flaws. – Tauren Jul 16 '10 at 17:03
  • @Tauren - aha, I knew there was something obvious I was missing :) Neat article - shame I hadn't found it before! – Matchu Jul 16 '10 at 17:16
1

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
Derek Ziemba
  • 2,467
  • 22
  • 22
1

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.

Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
0

I've created a simple NPM module that solves this - inherit-array. It basically does the following:

function toArraySubClassFactory(ArraySubClass) {
  ArraySubClass.prototype = Object.assign(Object.create(Array.prototype),
                                          ArraySubClass.prototype);

  return function () {
    var arr = [ ];
    arr.__proto__ = ArraySubClass.prototype; 

    ArraySubClass.apply(arr, arguments);

    return arr;
  };
};

After writing your own SubArray class you can make it inherit Array as follows:

var SubArrayFactory = toArraySubClassFactory(SubArray);

var mySubArrayInstance = SubArrayFactory(/*whatever SubArray constructor takes*/)
Amit Portnoy
  • 5,957
  • 2
  • 29
  • 30