How ECMAScript 5 still does not allow to subclass an array
- Why subclass an array?
- Naive approach
- Problems with naive approach
- Special nature of arrays
- Function objects and [[Construct]]
- The importance of array special behavior
- Existing solutions
- ECMAScript 5 accessors to the rescue
- [[Class]] limitations
- Does [[Class]] matter?
- Wrappers. Direct property injection.
- Wrappers. Prototype chain injection.
- Summary
Subclassing an array in Javascript has never been a trivial task. At least for a certain meaning of “subclassing an array”. Curiously, new edition of the language — ECMAScript 5 — still does not allow to fully subclass an array.
Not everything is lost though, and there are few ways ECMAScript 5 makes this task closer to the ideal. However, there are few fundamental issues which prevent true array subclassing from happening.
Let’s talk about that.
Today we’ll take a look at what it means to subclass an array, what some of the existing implementations/workarounds are, and which drawbacks those implementations have; We’ll see what ECMAScript 5 brings to the table, and what those fundamental issues with subclassing are. We’ll also talk about alternative approaches to subclassing an array, such as using wrappers, and get to know their limitations.
But first, what does it mean to subclass an array? And why do we even need it?
Why subclass an array?
We can define “subclassing an array” as the process of creating an object which inherits from native Array object (has Array.prototype in its prototype chain), and follows behavior similar (or identical) to native array.
The last point about behavior similar to native array is actually very important, as we’ll see later on. Having “subclass” of array could be thought of as being able to create an array object, but an object which would inherit not directly from Array, but from another object, and only then from Array.
In other words, we want behavior similar to this:
var sub = new SubArray(1, 2, 3);
sub; // [1, 2, 3]
sub.length; // 3
sub[1]; // 2
sub.push(4);
sub; // [1, 2, 3, 4]
// etc.
sub intanceof SubArray; // true
sub intanceof Array; // true
Note how SubArray constructor creates a sub object identical in its behavior to array (object has “length” property, numeric “0”, “1”, “2” properties, and inherits Array.prototype.* methods). At the same time, it is SubArray that a sub object directly inherits from, not Array.
So what exactly is the purpose of doing all this? Why subclass an array in such way?
There are usually two reasons:
1. Avoid pollution of global Array
Javascript prototypal nature makes it easy to extend all array objects with custom methods. Instead of assigning to direct properties of array objects, it’s much easier and more efficient to assign to array’s “prototype” object (the one that’s usually accessed via Array.prototype).
Array.prototype.last = function () {
return this[this.length-1];
};
// ...
[1, 2, 3].last(); // 3
However, extending Array.prototype comes with the price; And that price is chance of collisions. When scripts coexist with other scripts in an application, it’s important for those scripts not to conflict with each other. Extending Array.prototype, while tempting and seemingly useful, unfortunately isn’t very safe in a diverse environment. Different scripts can end up defining same-named methods, but with different behavior. Such scenario often leads to inconsistent behavior and hard-to-track errors.
Collisions can happen not only with user-defined code, but also with proprietary methods implemented by environment itself (e.g. Array.prototype.indexOf from JavaScript 1.6, before it was standardized by ES5) or from future standards (e.g. Array.prototype.map, Array.prototype.reduce, etc. — now all part of ES5).
Using constructor function other than native Array — but with same behavior — would allow to avoid such collisions. Instead of extending Array.prototype, another object would be extended (say, SubArray.prototype) and then used to initialize (sub)array objects. Any third party code which depends on methods from Array.prototype would still be able to safely use them.
2. Create data structures naturally inheriting from array
Another reason to subclass an array is to be able to create data structures, which naturally inherit from array; such as Stack, List, Queue, Set, etc. While there are certainly valid use cases for these structures, in this article I will instead focus on the first aspect — reducing chance of collisions. It is somewhat more relevant in context of cross-browser scripting.
Naive approach
Creating objects that inherit from other objects is more or less straightforward in Javascript. We can use well-known clone method:
function clone(obj) {
function F() { }
F.prototype = obj;
return new F();
}
and then set-up inheritance like this:
function Child() { }
Child.prototype = clone(Parent.prototype);
clone might look confusing, but all it does is create an object with another object as nearest ancestor in its prototype chain. It uses intermediate function to avoid executing “parent” constructor. In this example, new Child creates an object with Child.prototype as first object in the prototype chain, Parent.prototype — second, and so on. To visualize, the prototype chain here looks like this:
new Child()
|
| [[Prototype]]
|
v
Child.prototype
|
| [[Prototype]]
|
v
Parent.prototype
|
| [[Prototype]]
|
v
Object.prototype
|
| [[Prototype]]
|
v
null
Using clone method is exactly what person attempts when trying to subclass an array for the first time:
function SubArray() {
// Take any arguments passed to constructor and add them to an instance
this.push.apply(this, arguments);
}
SubArray.prototype = clone(Array.prototype);
var sub = new SubArray(1, 2, 3);
The approach seems reasonable. After all, the goal is to create an object that inherits from Array, so there’s no reason tried-and-true clone wouldn’t work. Or is there? As with few other things in Javascript, it’s not as trivial as it seems.
Problems with naive approach
So what exactly is wrong with subclassing array using clone method? Let’s take a look at how previously declared SubArray function behaves. We’ll be using native array object alongside, for comparison.
var arr = new Array(1, 2, 3);
var sub = new SubArray(1, 2, 3);
arr.length; // 3
sub.length; // 0 (in IE<8)
arr.length = 2;
sub.length = 2;
arr; // [1, 2]
sub; // [1, 2, 3]
arr[10] = 'foo';
sub[10] = 'foo';
arr.length; // 11
sub.length; // 2
There’s clearly some kind of inconsistency here. Even not counting a bug in IE<8. But what is this strange relation between length and numeric properties in array? And why doesn’t subclassed array behave identical? To understand this, we need to look into what array objects in Javascript really are.
Special nature of arrays
It turns out that arrays in Javascript are almost like plain Object objects, except for one little difference in behavior. The crux of this difference is summarized concisely in one paragraph of specification (15.4):
Array objects give special treatment to a certain class of property names. A property name P (in the form of a string value) is an array index if and only if ToString(ToUint32(P)) is equal to P and ToUint32(P) is not equal to 2^32 - 1. Every Array object has a length property whose value is always a nonnegative integer less than 2^32. The value of the length property is numerically greater than the name of every property whose name is an array index; whenever a property of an Array object is created or changed, other properties are adjusted as necessary to maintain this invariant. Specifically, whenever a property is added whose name is an array index, the length property is changed, if necessary, to be one more than the numeric value of that array index; and whenever the length property is changed, every property whose name is an array index whose value is not smaller than the new length is automatically deleted. This constraint applies only to properties of the Array object itself and is unaffected by length or array index properties that may be inherited from its prototype.
For those allergic to the condensed language of ECMA-262, here’s a short summary.
Array objects treat “numeric” properties in a special way. Whenever such property changes, value of array’s “length” property is adjusted as well; it’s adjusted in such was as to make sure that it is always one more than the greatest numeric (own) property of an array. Similarly, when “length” property is changed, numeric properties are adjusted accordingly (but only those that are larger than value of “length”).
We have already seen relation between numeric properties and length in the previous example, but let’s take a look at it again, step by step:
1) When array object is created, its “length” property is set to a value one more than the largest index of an array.
var arr = ['x', 'y', 'z'];
arr.length; // 3 (1 greater than largest index of an array — 2 in this case)
arr = ['foo'];
arr.length; // 1 (1 greater than largest index of an array — 0 in this case)
2) When numeric properties change, so does “length” change — to maintain the relationship of being 1 greater than the largest index.
var arr = ['x', 'y'];
arr.length; // 2, as expected
arr[2] = 'z'; // add another numeric property (2) larger than the largest existing one (1)
arr.length; // 3 — length is changed to be 1 greater than (new) largest index (2)
3) When “length” property changes, numeric properties are adjusted in such way so that greatest index is 1 smaller than value of “length”.
var arr = ['x', 'y', 'z'];
arr.length = 2;
arr; // ['x', 'y'] — note how last element (z) is deleted, because being at 2nd index,
// it doesn't satisfy criteria of largest index being 1 less than length
arr.length = 4;
arr; // ['x', 'y'] — "increasing" length doesn't affect numeric properties...
arr.join(); // "x,y,," ...but has consequences visible in other cases, such as when using `Array.prototype.join`
arr.push('z');
arr; // ['x', 'y', undefined, undefined, 'z'] — ...or when using `Array.prototype.push`
Now you know the “special” nature of Array objects in Javascript, which is in the relationship between “length” and numeric properties. One little detail we haven’t looked at is that array’s “length” property MUST always have a value of non-negative integer less than 2^32. Whenever this condition is violated, a RangeError is thrown:
var arr = [];
arr.length = Math.pow(2, 32); // RangeError
arr.length; // 0 (length is still 0, as it initially was)
arr.length = Math.pow(2, 32) - 1; // set length to maximum allowed value
arr.length++; // RangeError (when setting length explicitly)
arr.push(1); // RangeError (or when setting length implicitly)
Function objects and [[Construct]]
It should start to make sense why there are discrepancies in behavior of objects created via SubArray and Array functions. Even though SubArray creates an object that inherits from Array.prototype, that object completely lacks array’s special behavior. The SubArray instance is nothing more than a plain Object object (as if it was created via an object literal — { }).
But why does SubArray create an Object object and not an Array object? The core of this issue is in the way functions work in ECMAScript.
When new operator is applied to an object — as in new SubArray — that object’s internal [[Construct]] method is called. In our case, it is [[Construct]] of SubArray function. SubArray — being a native function — has [[Construct]] that’s specified to create a plain Object object, and invoke corresponding function providing newly created object as this value. Any native function, including SubArray, should create an Object object and return it as a result.
Now it’s worth mentioning that it’s possible to sort of supersede return value of [[Construct]] by explicitly returning non-primitive value from constructor function:
function SubArray() {
this.push.apply(this, arguments);
return []; // return array object explicitly
}
— but in that case, returned object does NOT inherit from constructor’s “prototype” (SubArray.prototype in this case); neither is constructor function invoked with that object as this value:
var sub = new SubArray(1, 2, 3);
// Object doesn't have 1, 2, 3, as constructor was never called with `this` value referencing returned object
sub; // []
// SubArray is not in the prototype chain of returned object
sub instanceof SubArray; // false
As you can see, creating an object that inherits from Array.prototype is only part of the story. The biggest issue is to preserve the special relation of length and numeric properties. This is why using regular clone approach is not quite up to the task.
The importance of array special behavior
A reasonable question at this point is — “Why does array special behavior matter”? Why would we want to preserve relationship between length and numeric properties when subclassing an array? It turns out that consequences of proper length are not only visible when working with length directly, but also indirectly, when performing other tasks via Array.prototype.* methods.
Take for example Array.prototype.push — a method to append items to the end of array. To determine from which position to start inserting elements into, push retrieves a value of array’s “length”. If length is not preserved properly, elements are inserted at the wrong location:
var arr = ['x', 'y'];
arr.length = 5;
arr.push('z'); // 'z' is inserted at 5th index, since that is what the value of "length" is
arr; // ['x', 'y', undefined, undefined, undefined, 'z']
Take another method — Array.prototype.join. Used to return a representation of an array by concatenating all elements with a separator, Array.prototype.join also uses length property to determine when to stop concatenating values:
var arr = ['x', 'y'];
arr.join(); // "x,y"
arr.length = 5;
arr.join(); // "x,y,,,"
Same goes for Array.prototype.concat — method used to produce a new array by concatenating values passed to concat:
var arr = ['x'];
arr.length = 3;
arr.concat('y'); // ['x', undefined, undefined, 'y']
Finally, the special behavior is often cleverly exploited in other situations, such as to “clear” an array (i.e. delete all of its numeric properties):
var arr = [1, 2, 3];
arr.length = 0;
arr; // [] — setting length to 0 effectively removes all numeric properties (elements) of an array
Existing solutions
Now that we're familiar with the theory, let's see what the situation is with subclassing arrays in practice. There have been few attempts in the past, with various levels of “success”. Here are a couple of most popular ones:
Andrea Giammarchi solution
One of the recent implementations is Stack, by Andrea Giammarchi, which looks like this:
var Stack = (function(){ // (C) Andrea Giammarchi - Mit Style License
function Stack(length) {
if (arguments.length === 1 && typeof length === "number") {
this.length = -1 < length && length === length << 1 >> 1 ? length : this.push(length);
}
else if (arguments.length) {
this.push.apply(this, arguments);
}
};
function Array() { };
Array.prototype = [];
Stack.prototype = new Array;
Stack.prototype.length = 0;
Stack.prototype.toString = function () {
return this.slice(0).toString();
};
Stack.prototype.constructor = Stack;
return Stack;
})();
It's an interesting solution, which mainly works around IE<8 bug with Array.prototype.push and length property. However, as should be obvious by now, it doesn't really solve the problem of maintaining relation between length and numeric properties:
var stack = new Stack('x', 'y');
stack.length; // 2
// so far so good
stack.push('z');
stack.length; // 3
// still good
stack[3] = 'foo';
stack.length; // 3
// not good anymore (length should have been changed to 4)
stack.length = 2;
stack[2]; // 'z'
// still not good (element at 2nd index should have been deleted)
Dean Edwards solution
Another popular solution is by Dean Edwards. This one takes a completely different approach — instead of creating an object that inherits from Array.prototype, an actual Array constructor is "borrowed" from the context of another iframe.
// create an <iframe>
var iframe = document.createElement("iframe");
iframe.style.display = "none";
document.body.appendChild(iframe);
// write a script into the <iframe> and steal its Array object
frames[frames.length - 1].document.write(
"<script>parent.Array2 = Array;<\/script>";
);
The reason this “works” is due to browsers creating separate execution environments for each frame in a document. Each such environment has a separate set of both — built-in and host objects. Built-in objects include global Array constructor, among others. Array object of one iframe is different from Array object of another iframe. They also don’t have any kind of hierarchical relation:
// assuming that SubArray was borrowed from another iframe
var sub = new SubArray(1, 2, 3);
sub instanceof SubArray; // true
sub instanceof Array; // false
sub instanceof Object; // false
Notice how sub is reported as NOT an instance of Array, and NOT an instance of Object. This is because neither Array, nor Object are anywhere in the prototype chain of sub object. Instead, prototype chain consists of SubArray.prototype, followed by <Object from another iframe>.prototype:
new SubArray()
|
| [[Prototype]]
|
v
.Array.prototype
|
| [[Prototype]]
|
v
.Object.prototype
|
| [[Prototype]]
|
v
null
This brings us to one “consideration” with this approach — difficulties determining the nature of an object derived from such iframe. It’s no longer possible to determine that an object is an array using instanceof or constructor checks [1]:
// is this object an array?
sub instanceof Array; // false
sub.constructor === Array; // false
It is, however, still possible to use [[Class]] check (we’ll talk about [[Class]] later on):
Object.prototype.toString.call(sub) === '[object Array]'; // true
Another, more inherent, downside of this approach is that it doesn’t work in non-browser environments (or, more precisely, in any environment without support for iframes). This problem is likely to become even bigger, given that server-side Javascript implementations are rising quite fast.
Finally, it was reported that Array borrowing can cause mixed content warning in IE6, among few other minor issues.
Other than that, iframe-based array “subclassing” is free of downsides of solutions like Stack, since we’re dealing with real array objects, and so proper length/indices relation.
ECMAScript 5 accessors to the rescue
But let’s talk about ECMAScript 5, which as I mentioned in the beginning, brings something that helps with subclassing arrays. This “something” is actually nothing but property accessors. These useful language constructs have been present in some popular implementations (SpiderMonkey, JavaScriptCore, and others) as a non-standard extension for quite a while now. They are now standardized by the new edition of the language.
Using accessors, it’s rather trivial to create an Object object with special length/indices relation — relation that’s identical to that of Array objects! And since we already know how to create an object with Array.prototype in its prototype chain, combining these two aspects would allow for a complete emulation of arrays.
There’s one little detail about implementation. Since ECMAScript (including last, 5th version) doesn’t provide any catch-all (aka __noSuchMethod__) mechanism, it’s not possible to change value of length property of an object when numeric property is modified; in other words, we can’t intercept the moment when ‘0’, ‘1’, ‘2’, ‘15’, etc. properties are being set. However, accessors allow us to intercept any read access of length property and return proper value, depending on which numeric properties object has at that moment. This is all we really need.
Here’s an implementation of it, at about 45 lines of code:
var makeSubArray = (function(){
var MAX_SIGNED_INT_VALUE = Math.pow(2, 32) - 1,
hasOwnProperty = Object.prototype.hasOwnProperty;
function ToUint32(value) {
return value >>> 0;
}
function getMaxIndexProperty(object) {
var maxIndex = -1, isValidProperty;
for (var prop in object) {
isValidProperty = (
String(ToUint32(prop)) === prop &&
ToUint32(prop) !== MAX_SIGNED_INT_VALUE &&
hasOwnProperty.call(object, prop));
if (isValidProperty && prop > maxIndex) {
maxIndex = prop;
}
}
return maxIndex;
}
return function(methods) {
var length = 0;
methods = methods || { };
methods.length = {
get: function() {
var maxIndexProperty = +getMaxIndexProperty(this);
return Math.max(length, maxIndexProperty + 1);
},
set: function(value) {
var constrainedValue = ToUint32(value);
if (constrainedValue !== +value) {
throw new RangeError();
}
for (var i = constrainedValue, len = this.length; i < len; i++) {
delete this[i];
}
length = constrainedValue;
}
};
methods.toString = {
value: Array.prototype.join
};
return Object.create(Array.prototype, methods);
};
})();
We can now create “sub arrays” via makeSubArray function. It accepts one argument — an object with methods to add to [[Prototype]] of returned “sub array”.
var subMethods = {
last: {
value: function() {
return this[this.length - 1];
}
}
};
var sub = makeSubArray(subMethods);
var sub2 = makeSubArray(subMethods);
// etc.
We can also hide this factory method behind a constructor, to make it similar to Array’s one:
var SubArray = (function() {
var methods = {
last: {
value: function() {
return this[this.length - 1];
}
}
};
return function() {
var arr = makeSubArray(methods);
if (arguments.length === 1) {
arr.length = arguments[0];
}
else {
arr.push.apply(arr, arguments);
}
return arr;
};
})();
And then use it as you would use regular Array constructor:
var sub = new SubArray(1, 2, 3);
sub.length; // 3
sub; // [1, 2, 3]
sub.length = 1;
sub; // [1]
sub[10] = 'x';
sub.push(1);
You can find this version of SubArray together with unit tests in Gtihub repository. For brevity, I made this implementation mainly take care of length/indices relation; certain methods (e.g. concat) do not behave identical to Array and need to be implemented accordingly.
[[Class]] limitations
The implementation we have just seen — the one utilizing property accessors — is great. It doesn’t require any host objects (such as iframes); it preserves relation between length and numeric properties; it even disallows out-of-range values for length or indices. All it requires is support for ES5 (or even just Object.create method).
But the dramatic title of this post is not there just for fun. There’s one little detail we’re missing in this otherwise complete implementation. And that detail is proper [[Class]] value — something that ECMAScript still doesn’t give full control over.
I wrote about [[Class]] before, when explaining how to detect arrays. In a nutshell, [[Class]] is an internal property of objects in ECMAScript. Its value is never exposed directly, but can still be inspected using certain methods (e.g. Object.prototype.toString). The usefulness of [[Class]] is that it allows to detect type of objects without relying on instanceof operator or checking object’s constructor — both of which fall short to detect objects from other contexts (e.g. iframes), as we’ve seen earlier.
Now, since objects created by makeSubArray are nothing but plain Object objects (only with special length getters/setters), their [[Class]] is also that of “Object” not an “Array”! We’ve taken care of length/indices relation, we’ve set up Array.prototype inheritance, but there’s no way to change object’s [[Class]] value. And so this solution can not claim to be complete.
Does [[Class]] matter?
You might be wondering — what are the actual implications of these pseudo-array objects having [[Class]] of “Object” not an “Array”. Do we even care? Well, for once, there’s an issue with object detection. Ironically, the solution I proposed to detect arrays relies on [[Class]], and so would fall short with objects like these.
// assuming that `sub` is a pseudo-array
Object.prototype.toString.call(sub) === '[object Array]'; // false
Another, probably more important, implication is that some of the methods in ECMAScript actually rely on [[Class]] value. For example, a well-known Function.prototype.apply accepts an array as its second argument (as well as an arguments object). Section 15.3.4.3 of ES3 says — “if argArray is neither an array nor an arguments object (see 10.1.8), a TypeError exception is thrown”. What this means is that if we pass pseudo-array object as a second argument to apply it will throw TypeError. apply doesn’t know or care if an object inherits from Array.prototype; neither does it care about object implementing special length/indices behavior. All it cares is that object is of proper type — type that we, unfortunately, can not emulate.
// assuming that `sub` is a pseudo-array
someFunction.apply(this, sub); // TypeError
There’s some vagueness in specification on this matter. For example, in Date.prototype.setTime spec says “If the this value is not a Date object, throw a TypeError exception.”, but in Date.prototype.getTime, it uses [[Class]] rather than just “not a Date object” — “If the this value is not an object whose [[Class]] property is “Date”, throw a TypeError exception”.
It’s probably safe to assume that these 2 phrases — “Date object” and “object with [[Class]] of ‘Date’” — have identical meaning. Ditto for “Array object” and “object with [[Class]] of ‘Array’”, as well as others.
Function.prototype.apply is not the only method sensitive to [[Class]] of an object. Array.prototype.concat, for example, follows different algorithm based on whether an object is an array or not (in other words — whether it has [[Class]] of “Array” or not).
// array ([[Class]] == "Array")
var arr = ['x', 'y'];
// object with numeric properties ([[Class]] == "Object")
var obj = { '0': 'x', '1': 'y' };
[1,2,3].concat(arr); // [1, 2, 3, 'x', 'y']
[1,2,3].concat(obj); // [1, 2, 3, { '0': 'x', '1': 'y' }]
As you can see, array values are “flattened”, whereas non-array ones are left as is. It is certainly possible to give these pseudo-arrays custom implementation of concat (and “fix” any other of Array.prototype.* methods), but the problem with Function.prototype.apply can not be solved.
It’s worth mentioning that another downside of accessor-based pseudo-array approach is performance. I haven’t done any tests, but it’s pretty clear that an implementation which has to enumerate over all numeric properties on every access of length property is not going to perform well. This is why I can't recommend this solution for anything other than educational purposes.
Wrappers. Direct property injection.
Realizing a somewhat futile nature of subclassing arrays in Javascript often makes alternative solutions look very attractive. One of such solutions is using wrappers. Wrapper approach avoids setting up inheritance or emulating length/indices relation. Instead, a factory-like function can create a plain Array object, and then augment it directly with any custom methods. Since returned object is an Array one, it maintains proper length/indices relation, as well as [[Class]] of “Array”. It also inherits from Array.prototype, naturally.
function makeSubArray() {
var arr = [ ];
arr.push.apply(arr, arguments);
arr.last = function() {
return this[this.length - 1];
};
return arr;
}
var sub = makeSubArray(1, 2, 3);
sub instanceof Array; // true
sub.length; // 3
sub.last(); // 3
While direct extension of array object is a beautiful, simplistic solution, it’s not without downsides. The main disadvantage is that on each invocation of constructor, an array needs to be extended with N number of methods. The time it takes to create an array is no longer a constant (if methods were on SubArray.prototype), but is directly proportional to the number of methods that need to be added.
Wrappers. Prototype chain injection.
To overcome the problem of “N methods”, another variation of wrappers can be used — the one in which object’s prototype chain is augmented, rather than object itself. Let’s see how this could be done:
function SubArray() { }
SubArray.prototype = new Array;
SubArray.prototype.last = function() {
return this[this.length - 1];
};
function makeSubArray() {
var arr = [ ];
arr.push.apply(arr, arguments);
arr.__proto__ = SubArray.prototype;
return arr;
}
The idea is simple. When makeSubArray function is executed, two things happen: 1) an array object is created and is populated with any passed arguments; 2) object’s prototype chain is augmented in such way so that next object is SubArray.prototype, not original Array.prototype. The augmentation of prototype chain is done via non-standard __proto__ property.
But what happens in makeSubArray function is of course only half of the story. To make sure that object has Array.prototype in its prototype chain, we need to make SubArray.prototype inherit from it. This is exactly what’s being done on a second line of this snippet (SubArray.prototype = new Array). Prototype chain of an object returned from makeSubArray now looks like this:
new SubArray()
|
| [[Prototype]]
|
v
SubArray.prototype
|
| [[Prototype]]
|
v
Array.prototype
|
| [[Prototype]]
|
v
Object.prototype
|
| [[Prototype]]
|
v
null
And because returned object is actually an Array, not an Object one, we also get length/indices relation as well as proper [[Class]] value. In fact, we can go even further and move initialization logic into SubArray constructor itself:
function SubArray() {
var arr = [ ];
arr.push.apply(arr, arguments);
arr.__proto__ = SubArray.prototype;
return arr;
}
SubArray.prototype = new Array;
SubArray.prototype.last = function() {
return this[this.length - 1];
};
var sub = new SubArray(1, 2, 3);
sub instanceof SubArray; // true
sub instanceof Array; // true
Even though augmenting prototype chain is a more performant solution, there’s a clear downside — it relies on non-standard __proto__ property. ECMAScript, unfortunately, does not allow to set [[Prototype]] of an object — internal property referencing immediate ancestor in its prototype chain. Not even in 5th edition. Even though __proto__ is supported by a rather large number of implementations, it is far from being truly compatible.
Summary
So here it is; all the fun intricacies of subclassing arrays in Javascript.
We’ve seen that contrary to what might seem, actual inheritance is by far not the only aspect of subclassing arrays in Javascript; that arrays are different from regular objects by having special length/indices relation; how this length/indices relation is important and has nothing to do with prototype chain of an object; how arrays have special [[Class]] value of “Array” which is also rather important, and isn’t inherited either; how it’s not possible to change [[Class]] value of an object — not even in ECMAScript 5. We looked at different ways to “subclass” an array, starting from borrowing Array constructors from other contexts, and ending with augmentation of prototype chain. We examined benefits and downsides of each one of those solutions.
What we haven’t touched upon is the performance metrics of each of the implementations — perhaps a good topic for another discussion.
On this note, I leave you with a table summarizing pros/cons of the above mentioned techniques.
| Proper [[Class]] | length/indices | Uses native objects only | Requires ES3 only | |
|---|---|---|---|---|
| Stack (Andrea Giammarchi) | No | No | Yes | Yes |
| IFrame borrowing (Dean Edwards) | Yes | Yes | No | Yes |
| Accessors | No | Yes | Yes | No |
| Direct extension | Yes | Yes | Yes | Yes |
| Prototype extension | Yes | Yes | Yes | No |
[1] Whether this endeavor is something worth pursuing is a topic for another discussion
P.S. Big thanks to John David Dalton for reviewing an article and giving useful suggestions.
Angus Croll said:
#Excellent, excellent article. Thank you!
I have mixed feelings about ECMA 5 exposing so many internal properties – I worry about them clouding the syntactic waters. But you’ve demonstrated that when used with good encapsulation (and faced with Array’s “special case”) they present a perfectly appropriate solution.
Cedric Dugas said:
#Always learn new stuff when I comes here,
Personally I would have stop at the naive approach, thx for the heads up!
Peter van der Zee said:
#Hi Kangax,
Good writeup!
You may want to explain _why_ it’s so difficult to get the length property to behave as desired. (The whole setting an own property overrides the property in the prototypal chain thing and that being the reason length wont do it’s special voodoo :)) Or have I missed that…?
Oh and in ES5 it’s no longer illegal to pass non-Array non-Arguments objects to apply.
Mariusz Nowak said:
#I’m impressed. One of the best and most important articles I’ve read in last months. Thanks!
In my work I’ve sticked to Direct extension but now I see that Prototype extension is well worth using in some implementations.
Ywg said:
#Brilliant, as usual !
kangax (article author) said:
#Peter,
I’m not sure I understand what you mean by difficulties getting
lengthto behave as desired. Mind clarifying?But the point about
Function.prototype.applyaccepting non-Array objects in ES5 is excellent :) Thanks for bringing it up; I have completely missed it. This actually changes things slightly, meaning that in conforming ES5 implementations pseudo-array objects are not as useless as they are in ES3. It’s as if ES5 — by changing algorithm ofapply— is trying to alleviate the pain with subclassing arrays (and probably also to allow passing array-like objects, such as certain host ones).Unfortunately,
applyisn’t the only issue, and pseudo-arrays are still not a perfect replacement.Performance considerations aside, I see at least 3 other places in which [[Class]] of an object plays a key role. One is
Array.isArray, which returnstrueif and only if given object has [[Class]] of an “Array”. Another one —JSON.stringify, which considers second argument to be a whiltelist collection if its [[Class]] is an “Array”. Finally,JSON.parseforks its algorithm based on a [[Class]] value.It’s also interesting how pseudo-arrays which go through
JSON.parse/stringifylose their “special” nature:var arr = ['x', 'y']; JSON.parse(JSON.stringify(arr)); // still an array // let's try with pseudo-array var sub = new SubArray('x', 'y'); sub.length; // 2 sub = JSON.parse(JSON.stringify(sub)); sub.length; // undefined < -- lost length sub instanceof Array; // false <-- prototype chain is messed upPeter van der Zee said:
#Haha, to be honest I doubt that Array subclassing was any consideration for lifting the array restriction on apply, but it doesn’t really matter in the end :)
What I mean is that the length property of array is a special getter. If you set it, magic things happen (as you explain). However, when subclassing the property, the magic getter/setter will be in the prototypal chain. So as you explained, as long as you read from it, it will do the proper voodoo when setting an index property. However, once you set it, it becomes an own property of the instance of the subclass and it no longer carries the setter properties (of deleting index properties higher or equal to the new length value). This is simply because you cannot change the property of the original array length property by setting it on an instance of an inherited class.
However, ES5 made an important change. Using getters and setters you can create proxies and access the length property through the prototypal chain? `subArray.prototype.length = 5;` If you make the SubArray.prototype.length a getter/setter, you can map it to this.prototype.length…
I hope you get what I’m trying to say :) And hope I’m not messing stuff up, it’s late here.
Peter van der Zee said:
#Hm, can you actually “access” a setter that’s on the prototype chain? Or will it create a new property anyways. I’m not sure now. If it creates a new property anyways the initial problem holds, even in ES5. Namely: because setting obj.length creates a new property on obj if the property is only found in the prototypal chain, subsequent reads will not reflect implicit changes to the length property.
kangax (article author) said:
#Peter,
I see what you mean. Only ES5 doesn’t seem to expose array’s accessor logic via getter/setter. 15.4.5.2 says that “length” property of array instances is a data property. So we can’t get access to a setter via
Object.getOwnPropertyDescriptor([], 'length').set. The logic is encapsulated on the level of implementation.Peter van der Zee said:
#Ah right. Then the possible solution is mute and the problem remains unfixable at any level.
Dan Beam said:
#Hey kangax, how about adding JSON.parse and JSON.stringify (and possibly JSON.stringifier) to the list. It can’t hurt. It’s what Crockford would want, haha – http://www.json.org/js.html.
Asen Bozhilov said:
#I can not belive that ES use [[Class]] property for recognizing object types. I did some tests with
iframeand from what I saw you are correct aboutFunction.prototype.applyand recognition by [[Class]] value.Why I can not belive that is true. First of all, more reliable approach is to compare [[Prototype]] value against built in prototype. For example:
Of course that is not solve the problem with cross frame/window scripting, but I am not sure it must to does. Is it ECMAScript design is according browser scripting issues? How do you think?
David Bruant said:
#Excellent article!
Just about the iframe strategy, I think that a somewhat “cleaner” method would be to do the following (in the parent context) :
Irakli Gozalishvili said:
#I do think you have incorrect expectations. There is a solution to add extend arrays in ES5 to get all the functionality you need but it’s not a sub-classing (see extend-array.js). https://gist.github.com/666251
Subclassing is also possible in ES5 without __proto__ (see sub-class-array.js)
Of course sub[10] = ‘foo’; will not work as you expect, neither it is supposed to since sub will get a property with name ’10′ and SubArray does not defines any special handling.
In any case if you need to extend behavior of the arrays extend-array.js is a way to go IMO.
kangax (article author) said:
#@Irakli Gozalishvili
Your extend-array.js is basically a “direct injection” approach from the post (with same pros and cons — which I mentioned in corresponding section); only that in ES3 you simply assign a property, and in ES5 — there’s also a possibility to use
defineProperty/defineProperties/etc. (controlling property attributes).Whether it’s correct or incorrect expectations to have array-like objects with same length/indices relation is a subjective matter. Even if we _don’t_ expect such relation, there’s still a matter of [[Class]] of “Object” on array-like objects, which leads to
Array.isArrayreturningfalse;JSON.parse/JSON.stringifyoperating on array-like objects as _objects_, not arrays; etc.I also think that expecting array-like objects to have magic length/indices is not unreasonable. When you detect that object is an array (for example, via
instanceofoperator), you might want to iterate over it. Using good oldforloop relies on length always being 1 larger than largest index property; there’s a special relation for you. You might also want to check if an array-like object is not empty, by checking its length property; there’s a special relation again.Also see “The importance of array special behavior” section (listing more use cases).
ekaj said:
#Hello,
there’s typos. You wrote `intanceof’ twice
Anonymous said:
#Did anybody else see the irony of someone correcting a typo regarding “intanceof”, lol?
Henri Manson said:
#I ran into the array subclassing problem as well, until I discovered that in my code I only used the array for indexing and not using any Array functions. In that case you can simply add numeric properties to an object which can be subclassed easily.
function ObjectArray(el1, el2, el3) { this[0] = el1; this[1] = el2; this[2] = el3; }; ObjectArray.prototype.sum = function() { var sum = 0; var i; // loop over numeric object elements for (i = 0; i < 3; i++) sum += this[i]; return sum; } var obj = new ObjectArray(10, 5, 6); var total = obj.sum(); // 21 var objtype = typeof obj; // objectPeter Rust said:
#kangax,
Thanks for the excellent article & research! I forked your array_subclassing repository and wrote a performant length getter that works for contiguous arrays. It does not properly calculate the length of sparse arrays, but I think that may be a shortcoming I can live with (I can’t remember the last time I needed or used a sparse array).
It caches the length property, so it should be fairly performant, though there is still room for optimization. When the cached length is no longer up-to-date, it starts there and searches outwards for the new length by powers of 2 and then zeroes in on it with a binary search.
What do you think?
– Peter Rust
Developer, Cornerstone Systems
serie tele said:
#It seems this web site doesnt display correctly using a Dell Streak. Are other people getting the precise same difficulty ?