Subclassing Javascript Arrays. TypeError: Array.prototype.toString is not generic
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
?
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:
- How ECMAScript 5 still does not allow to subclass an array
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
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.
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
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