23

Is there a data structure or a pattern in JavaScript that can be used for both fast lookup (by key, as with associative arrays) and for ordered looping?

Right, now I am using object literals to store my data, but I just discovered that Chrome does not maintain the order when looping over the property names.

Is there a common way to solve this in JavaScript?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Haes
  • 12,891
  • 11
  • 46
  • 50

3 Answers3

35

Create a data structure yourselves. Store the ordering in an array that is internal to the structure. Store the objects mapped by a key in a regular object. Let's call it OrderedMap which will have a map, an array, and four basic methods.

OrderedMap
    map
    _array

    set(key, value)
    get(key)
    remove(key)
    forEach(fn)

function OrderedMap() {
    this.map = {};
    this._array = [];
}

When inserting an element, add it to the array at the desired position as well as to the object. Insertion by index or at the end is in O(1).

OrderedMap.prototype.set = function(key, value) {
    // key already exists, replace value
    if(key in this.map) {
        this.map[key] = value;
    }
    // insert new key and value
    else {
        this._array.push(key);
        this.map[key] = value;
    }
};

When deleting an object, remove it from the array and the object. If deleting by a key or a value, complexity is O(n) since you will need to traverse the internal array that maintains ordering. When deleting by index, complexity is O(1) since you have direct access to the value in both the array and the object.

OrderedMap.prototype.remove = function(key) {
    var index = this._array.indexOf(key);
    if(index == -1) {
        throw new Error('key does not exist');
    }
    this._array.splice(index, 1);
    delete this.map[key];
};

Lookups will be in O(1). Retrieve the value by key from the associative array (object).

OrderedMap.prototype.get = function(key) {
    return this.map[key];
};

Traversal will be ordered and can use either of the approaches. When ordered traversal is required, create an array with the objects (values only) and return it. Being an array, it would not support keyed access. The other option is to ask the client to provide a callback function that should be applied to each object in the array.

OrderedMap.prototype.forEach = function(f) {
    var key, value;
    for(var i = 0; i < this._array.length; i++) {
        key = this._array[i];
        value = this.map[key];
        f(key, value);
    }
};

See Google's implementation of a LinkedMap from the Closure Library for documentation and source for such a class.

Anurag
  • 140,337
  • 36
  • 221
  • 257
  • How are lookups O(1)? If you are using anything but an array index, JS property resolution is O(n) (it has to resolve by looping through the prototype chain) See Property Lookup on http://bonsaiden.github.io/JavaScript-Garden/ – ginman Nov 05 '14 at 20:23
  • 4
    The fact that it has to traverse up the prototype chain still doesn't make it O(n). Imagine a prototype chain hierarchy 4 levels deep and each level maintains a hash structure to hold the keys and values. Then it will only require, on average, 4 such O(1) lookups. That's still O(1). Now, as far as I know, ECMAScript doesn't mandate implementation details so someone can do it in O(n²) if they wanted to. In other words, it's implementation dependent, but given most decent implementations, you can expect O(1) lookups on average. – Anurag Nov 05 '14 at 23:40
  • "O(1) describes an algorithm that will always execute in the same time (or space) regardless of the size of the input data set." i.e. a hashmap lookup, which doesnt change based on the size of the hashmap. This lookup will always change based on the number of properties and prototypes in the chain. That makes it O(n). It still progresses linearly, but the lookups are never O(1). http://rob-bell.net/2009/06/a-beginners-guide-to-big-o-notation/ – ginman Dec 18 '14 at 20:43
  • 3
    There are two variables you are discussing. The first is the number of items in the hash map, and the second is the depth of the prototype chain. The depth of the prototype chain (in this particular scenario) is a constant and does not grow as the number of items in the hash map increases. With that, please re-read my previous statement and it will all make sense to you now. If not, please check out this [article](http://en.wikipedia.org/wiki/Big_O_notation). – Anurag Dec 19 '14 at 20:54
  • 1
    @ginman It's not O(n), though; the O applies to the number of items you're looking up. Adding a constant (the prototype chain) doesn't change this, and your own quote "[...] regardless of the site of the input dataset" says the same thing. – Dave Newton Jun 11 '15 at 17:58
  • Linked map link is dead – TripleS Sep 02 '16 at 15:11
3

The only instance in which Chrome doesn't maintain the order of keys in an object literal seems to be if the keys are numeric.

  var properties = ["damsonplum", "9", "banana", "1", "apple", "cherry", "342"];
  var objLiteral = {
    damsonplum: new Date(),
    "9": "nine",
    banana: [1,2,3],
    "1": "one",
    apple: /.*/,
    cherry: {a: 3, b: true},
    "342": "three hundred forty-two"
  }
  function load() {
    var literalKeyOrder = [];
    for (var key in objLiteral) {
      literalKeyOrder.push(key);
    }

    var incremental = {};
    for (var i = 0, prop; prop = properties[i]; i++) {
      incremental[prop] = objLiteral[prop];
    }

    var incrementalKeyOrder = [];
    for (var key in incremental) {
      incrementalKeyOrder.push(key);
    }
    alert("Expected order: " + properties.join() +
          "\nKey order (literal): " + literalKeyOrder.join() +
          "\nKey order (incremental): " + incrementalKeyOrder.join());
  }

In Chrome, the above produces: "1,9,342,damsonplum,banana,apple,cherry".

In other browsers, it produces "damsonplum,9,banana,1,apple,cherry,342".

So unless your keys are numeric, I think even in Chrome, you're safe. And if your keys are numeric, maybe just prepend them with a string.

jhurshman
  • 5,861
  • 2
  • 26
  • 16
2

As has been noted, if your keys are numeric you can prepend them with a string to preserve order.

var qy = {
  _141: '256k AAC',
   _22: '720p H.264 192k AAC',
   _84: '720p 3D 192k AAC',
  _140: '128k AAC'
};

Example

Community
  • 1
  • 1
Zombo
  • 1
  • 62
  • 391
  • 407