Implement Array-like behavior in JavaScript without using Array
Now we have ECMAScript 2015 (ECMA-262 6th Edition; ES6), we have proxy objects, and they allow us to implement the Array
behaviour in the language itself, something along the lines of:
function FakeArray() {
const target = {};
Object.defineProperties(target, {
"length": {
value: 0,
writable: true
},
[Symbol.iterator]: {
// http://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype-@@iterator
value: () => {
let index = 0;
return {
next: () => ({
done: index >= target.length,
value: target[index++]
})
};
}
}
});
const isArrayIndex = function(p) {
/* an array index is a property such that
ToString(ToUint32(p)) === p and ToUint(p) !== 2^32 - 1 */
const uint = p >>> 0;
const s = uint + "";
return p === s && uint !== 0xffffffff;
};
const p = new Proxy(target, {
set: function(target, property, value, receiver) {
// http://www.ecma-international.org/ecma-262/6.0/index.html#sec-array-exotic-objects-defineownproperty-p-desc
if (property === "length") {
// http://www.ecma-international.org/ecma-262/6.0/index.html#sec-arraysetlength
const newLen = value >>> 0;
const numberLen = +value;
if (newLen !== numberLen) {
throw RangeError();
}
const oldLen = target.length;
if (newLen >= oldLen) {
target.length = newLen;
return true;
} else {
// this case gets more complex, so it's left as an exercise to the reader
return false; // should be changed when implemented!
}
} else if (isArrayIndex(property)) {
const oldLenDesc = Object.getOwnPropertyDescriptor(target, "length");
const oldLen = oldLenDesc.value;
const index = property >>> 0;
if (index > oldLen && oldLenDesc.writable === false) {
return false;
}
target[property] = value;
if (index > oldLen) {
target.length = index + 1;
}
return true;
} else {
target[property] = value;
return true;
}
}
});
return p;
}
I can't guarantee this is actually totally correct, and it doesn't handle the case where you alter length to be smaller than its previous value (the behaviour there is a bit complex to get right; roughly it deletes properties so that the length
property invariant holds), but it gives a rough outline of how you can implement it. It also doesn't mimic behaviour of [[Call]] and [[Construct]] on Array
, which is another thing you couldn't do prior to ES6—it wasn't possible to have divergent behaviour between the two within ES code, though none of that is hard.
This implements the length
property in the same way the spec defines it as working: it intercepts assignments to properties on the object, and alters the length
property if it is an "array index".
Unlike what one can do with ES5 and getters, this allows one to get length
in constant time (obviously, this still depends on the underlying property access in the VM being constant time), and the only case in which it provides non-constant time performance is the not implemented case when newLen - oldLen
properties are deleted (and deletion is slow in most VMs!).
Yes, you can subclass an array into an arraylike object easily in JavaScript:
var ArrayLike = function() {};
ArrayLike.prototype = [];
ArrayLike.prototype.shuffle = // ... and so on ...
You can then instantiate new array like objects:
var cards = new Arraylike;
cards.push('ace of spades', 'two of spades', 'three of spades', ...
cards.shuffle();
Unfortunately, this does not work in MSIE. It doesn't keep track of the length
property. Which rather deflates the whole thing.
The problem in more detail on Dean Edwards' How To Subclass The JavaScript Array Object. It later turned out that his workaround wasn't safe as some popup blockers will prevent it.
Update: It's worth mentioning Juriy "kangax" Zaytsev's absolutely epic post on the subject. It pretty much covers every aspect of this problem.
[] operator is the native way to access to object properties. It is not available in the language to override in order to change its behaviour.
If what you want is return computed values on the [] operator, you cannot do that in JavaScript since the language does not support the concept of computed property. The only solution is to use a method that will work the same as the [] operator.
MyClass.prototype.getItem = function(index)
{
return {
name: 'Item' + index,
value: 2 * index
};
}
If what you want is have the same behaviour as a native Array in your class, it is always possible to use native Array methods directly on your class. Internally, your class will store data just like a native array does but will keep its class state. jQuery does that to make the jQuery class have an array behaviour while retaining its methods.
MyClass.prototype.addItem = function(item)
{
// Will add "item" in "this" as if it was a native array
// it will then be accessible using the [] operator
Array.prototype.push.call(this, item);
}