19

Is there any way to create an array-like object in JavaScript, without using the built-in array? I'm specifically concerned with behavior like this:

var sup = new Array(5);
//sup.length here is 0
sup[0] = 'z3ero';
//sup.length here is 1
sup[1] = 'o3ne';
//sup.length here is 2
sup[4] = 'f3our';        
//sup.length here is 5

The particular behavior I'm looking at here is that sup.length changes without any methods being called. I understand from this question that the [] operator is overloaded in the case of arrays, and this accounts for this behavior. Is there a pure-javascript way to duplicate this behavior, or is the language not flexible enough for that?

According to the Mozilla docs, values returned by regex also do funky things with this index. Is this possible with plain javascript?

Community
  • 1
  • 1
Claudiu
  • 224,032
  • 165
  • 485
  • 680

10 Answers10

23

[] 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);
}
Vincent Robert
  • 35,564
  • 14
  • 82
  • 119
  • 3
    As of JavaScript 1.8.5 you *can* have computed properties. https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/defineProperty – mpen Feb 12 '13 at 16:31
  • @mpen and ES5, for those who care about the specs and what other browsers do (given the JS version numbers basically just encode SpiderMonkey releases!). – gsnedders Jun 10 '16 at 22:46
13

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.

Borgar
  • 37,817
  • 5
  • 41
  • 42
  • There are some alternative workarounds in the comments of Dean's post that seem to work pretty well (except for some performance issues). – Matt Kantor Dec 14 '08 at 18:16
  • This one in particular looks interesting: http://webreflection.blogspot.com/2008/03/sorry-dean-but-i-subclassed-array-again.html – Matt Kantor Dec 14 '08 at 18:17
10

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!).

Brett Zamir
  • 14,034
  • 6
  • 54
  • 77
gsnedders
  • 5,532
  • 2
  • 30
  • 41
  • One note: isArrayIndex errors when parameter `p` is a Symbol: "Cannot convert a Symbol value to a number". In my case, I added a check for `typeof p === 'symbol'` – moron4hire Jan 10 '18 at 04:30
  • 1
    @BrettZamir I am fine with my original version being used under the MIT license (© 2016 Sam Sneddon), but I can't speak for Filip Dupanović who did a fair bit more to it since then, so for the current version you'd also need to get their permission. – gsnedders Jan 15 '20 at 13:29
3

Is this what you're looking for?

Thing = function() {};
Thing.prototype.__defineGetter__('length', function() {
    var count = 0;
    for(property in this) count++;
    return count - 1; // don't count 'length' itself!
});

instance = new Thing;
console.log(instance.length); // => 0
instance[0] = {};
console.log(instance.length); // => 1
instance[1] = {};
instance[2] = {};
console.log(instance.length); // => 3
instance[5] = {};
instance.property = {};
instance.property.property = {}; // this shouldn't count
console.log(instance.length); // => 5

The only drawback is that 'length' will get iterated over in for..in loops as if it were a property. Too bad there isn't a way to set property attributes (this is one thing I really wish I could do).

Matt Kantor
  • 1,704
  • 1
  • 19
  • 37
2

The answer is: there's no way as of now. The array behavior is defined in ECMA-262 as behaving this way, and has explicit algorithms for how to deal with getting and setting of array properties (and not generic object properties). This somewhat dismays me =(.

Claudiu
  • 224,032
  • 165
  • 485
  • 680
1

You could also create your own length method like:

Array.prototype.mylength = function() {
    var result = 0;
    for (var i = 0; i < this.length; i++) {
        if (this[i] !== undefined) {
            result++;
        }
    }
    return result;
}
olleicua
  • 2,039
  • 2
  • 21
  • 33
1

Mostly you don't need a predefined index-size for arrays in javascript, you can just do:

var sup = []; //Shorthand for an empty array
//sup.length is 0
sup.push(1); //Adds an item to the array (You don't need to keep track of index-sizes)
//sup.length is 1
sup.push(2);
//sup.length is 2
sup.push(4);
//sup.length is 3
//sup is [1, 2, 4]
finpingvin
  • 692
  • 1
  • 7
  • 14
  • BTW, to answer your specific question more; An custom object that keeps better track on array-indices, if they are important for your cause, perhaps would store an internal array and getter/setter methods for this. The object could fill empty indices with 'null' to represent array.length better. – finpingvin Dec 14 '08 at 01:43
1

If you're concerned about performance with your sparse array (though you probably shouldn't be) and wanted to ensure that the structure was only as long as the elements you handed it, you could do this:

var sup = [];
sup['0'] = 'z3ero';
sup['1'] = 'o3ne';
sup['4'] = 'f3our';        
//sup now contains 3 entries

Again, it's worth noting that you won't likely see any performance gain by doing this. I suspect that Javascript already handles sparse arrays quite nicely, thank you very much.

Jason Kester
  • 5,951
  • 9
  • 35
  • 40
  • I believe your code is actually exactly equivalent to my code =P. [] is a literal for "new Array()", and sup[0] and sup['0'] behave identically. – Claudiu Dec 14 '08 at 01:59
  • oh sorry, i didn't see that you didn't have the initial index set up. – Claudiu Dec 14 '08 at 02:07
  • Arrays and Hashtables (and thus, Objects) are all closely related in Javascript, and there's not much distinction between them. An array with string keys behaves like a Hashtable, so in the example above it wouldn't pad itself out with values between 1 and 4. – Jason Kester Dec 16 '08 at 21:56
1

Interface and implementation

The case is a simple implementation of the original array packaging, you can replace the data structure and refer to the common interface can be implemented.

export type IComparer<T> = (a: T, b: T) => number;

export interface IListBase<T> {
  readonly Count: number;
  [index: number]: T;
  [Symbol.iterator](): IterableIterator<T>;
  Add(item: T): void;
  Insert(index: number, item: T): void;
  Remove(item: T): boolean;
  RemoveAt(index: number): void;
  Clear(): void;
  IndexOf(item: T): number;
  Sort(): void;
  Sort(compareFn: IComparer<T>): void;
  Reverse(): void;
}


export class ListBase<T> implements IListBase<T> {
  protected list: T[] = new Array();
  [index: number]: T;
  get Count(): number {
    return this.list.length;
  }
  [Symbol.iterator](): IterableIterator<T> {
    let index = 0;
    const next = (): IteratorResult<T> => {
      if (index < this.Count) {
        return {
          value: this[index++],
          done: false,
        };
      } else {
        return {
          value: undefined,
          done: true,
        };
      }
    };

    const iterator: IterableIterator<T> = {
      next,
      [Symbol.iterator]() {
        return iterator;
      },
    };

    return iterator;
  }
  constructor() {
    return new Proxy(this, {
      get: (target, propKey, receiver) => {
        if (typeof propKey === "string" && this.isSafeArrayIndex(propKey)) {
          return Reflect.get(this.list, propKey);
        }
        return Reflect.get(target, propKey, receiver);
      },
      set: (target, propKey, value, receiver) => {
        if (typeof propKey === "string" && this.isSafeArrayIndex(propKey)) {
          return Reflect.set(this.list, propKey, value);
        }
        return Reflect.set(target, propKey, value, receiver);
      },
    });
  }
  Reverse(): void {
    throw new Error("Method not implemented.");
  }
  Insert(index: number, item: T): void {
    this.list.splice(index, 0, item);
  }
  Add(item: T): void {
    this.list.push(item);
  }
  Remove(item: T): boolean {
    const index = this.IndexOf(item);
    if (index >= 0) {
      this.RemoveAt(index);
      return true;
    }
    return false;
  }
  RemoveAt(index: number): void {
    if (index >= this.Count) {
      throw new RangeError();
    }
    this.list.splice(index, 1);
  }
  Clear(): void {
    this.list = [];
  }
  IndexOf(item: T): number {
    return this.list.indexOf(item);
  }
  Sort(): void;
  Sort(compareFn: IComparer<T>): void;
  Sort(compareFn?: IComparer<T>) {
    if (typeof compareFn !== "undefined") {
      this.list.sort(compareFn);
    }
  }
  private isSafeArrayIndex(propKey: string): boolean {
    const uint = Number.parseInt(propKey, 10);
    const s = uint + "";
    return propKey === s && uint !== 0xffffffff && uint < this.Count;
  }
}

Case

const list = new List<string>(["b", "c", "d"]);
const item = list[0];

Reference

DasonCheng
  • 69
  • 5
-2

Sure, you can replicate almost any data structure in JavaScript, all the basic building blocks are there. What you'll end up will be slower and less intuitive however.

But why not just use push/pop ?

Mark Renouf
  • 30,697
  • 19
  • 94
  • 123
  • this is more a theoretical question about the limits of javascript than anything practical =P. i think my answer is that you can't do this. – Claudiu Dec 14 '08 at 01:36