3

I'm using a JS array to Map IDs to actual elements, i.e. a key-value store. I would like to iterate over all elements. I tried several methods, but all have its caveats:

for (var item in map) {...}

Does iterates over all properties of the array, therefore it will include also functions and extensions to Array.prototype. For example someone dropping in the Prototype library in the future will brake existing code.

var length = map.lenth;
for (var i = 0; i < length; i++) {
  var item = map[i];
  ...
}

does work but just like

$.each(map, function(index, item) {...});

They iterate over the whole range of indexes 0..max(id) which has horrible drawbacks:

var x = [];
x[1]=1;
x[10]=10;
$.each(x, function(i,v) {console.log(i+": "+v);});

0: undefined
1: 1
2: undefined
3: undefined
4: undefined
5: undefined
6: undefined
7: undefined
8: undefined
9: undefined
10: 10

Of course my IDs wont resemble a continuous sequence either. Moreover there can be huge gaps between them so skipping undefined in the latter case is unacceptable for performance reasons. How is it possible to safely iterate over only the defined elements of an array (in a way that works in all browsers and IE)?

sibidiba
  • 6,270
  • 7
  • 40
  • 50

7 Answers7

4

There are three issues:

  1. You should not use for...in to iterate arrays.
  2. You are using the wrong data type for your requirements.
  3. You are not using for...in correctly.

If you want to have something like a hash table then use a plain object:

var map = {};
map[123] = 'something';
map.foo = 'bar';
// same as map['foo'] = 'bar';
//...

It looks like an array, but it is not. It is an object with property 123. You can use either dot notation obj.key (only if the key is a valid identifier - 123 would not be valid so you have to use the following notation) or array notation obj['key'] to access object properties.

It seems that an object would be a more appropriate data structure.

But even then you should make a call to hasOwnProperty (every time you use for...in):

for(var key in obj) {
    if(obj.hasOwnProperty(key)) {
        //do something
    }
}

This checks whether a property is inherited from the prototype (it will return false then) or is truly an own property.

Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
4

Use hasOwnProperty within for ... in to make sure that prototype additions aren't included:

for (var item in map)
  if (map.hasOwnProperty(item)) {
    // do something
  }
casablanca
  • 69,683
  • 7
  • 133
  • 150
3

Use the EcmaScript 5 builtin Object.keys, and on non ES5 browsers, define it thus:

Object.keys = function (o) {
  var keys = [];
  var hasOwnProp = Object.prototype.hasOwnProperty;
  if (Object.prototype.toString.call(o) === '[object Array]') {
    for (var k in o) {
      if (+k === (k & 0x7fffffff) && hasOwnProp.call(o, k)) {
        keys[keys.length] = k;
      }
    }
    keys.sort(keys, function (a, b) { return a - b; });
  } else {
    for (var k in o) {
      if (hasOwnProp.call(o, k)) {
        keys[keys.length] = k;
      }
    }
  }
  return keys;
};
Mike Samuel
  • 118,113
  • 30
  • 216
  • 245
2

1) use an object like already suggested, it is by far the best solution.

2) if you for some reason need to use an array - don't be scared looping over it with

for(var i, len = arr.length;len < i;i++)

it's very very fast.

3) don't use $.each or similar methods if you want performance - they create a new callstack for every iteration, which is a huge overhead.

Martin Jespersen
  • 25,743
  • 8
  • 56
  • 68
  • What do you mean by using an object? – sibidiba Jan 12 '11 at 17:43
  • Use an object as a map to do lookups against: var map = {}; map[id] = true; if(map[id]) { ...} – Martin Jespersen Jan 12 '11 at 17:55
  • -1 if(map[id]} shouldn't be used, because it will look to the prototype. – goat Jan 12 '11 at 18:00
  • An "object" and an "array" is the same thing is javascript. – sibidiba Jan 12 '11 at 18:01
  • 3
    @sibidiba: No it is not. An array *is* an object but an object is not an array. In JS, an array is a numerical array with continuous indices while an object would be more *like* an associative array or a *map*. Arrays are defined via `[]` whereas objects are defined via `{}`. They are **not** the same. Run your the last example with `var x = {};` instead of `var x = [];` and you will see the difference. – Felix Kling Jan 12 '11 at 18:09
  • @sibidiba: That is not correct. Just because an Array is a type of Object, doesn't mean it is the same thing, any more than a Date and an Object are the same thing. They're both objects, but I'd hardly call them the same. – user113716 Jan 12 '11 at 18:10
0

Don't use an array. Use an object hash instead

var map = {};
map[key] = value;
...
for (var key in map) {
   do something to map[key]
}
Vadim
  • 17,897
  • 4
  • 38
  • 62
  • This is exactly what I'm doing. There is no difference. for (var key in map) will return object's/array's member functions too. – sibidiba Jan 12 '11 at 17:44
  • @sibidiba: There is a difference. An *object literal* as used in this answer from @Yads should not have its prototype affected at all, and as such, a `for/in` would work. If you run into member functions, then it would be because you're using some code that has extended `Object.prototype`, which is a really bad idea. Since numeric order is not a concern, use an object literal instead of an array literal. – user113716 Jan 12 '11 at 17:51
  • @patrick dw: But that is what sibidiba is referring to. Someone might add the Prototype library, which extends the `Object.prototype` if I'm not wrong. – Felix Kling Jan 12 '11 at 17:52
  • @Felix Kling: No, prototypejs doesn't extend `Object.prototype`. They only use the `Object` namespace to hold utilities. The object being operated on needs to be passed as the first parameter. [From the docs:](http://api.prototypejs.org/language/Object/) *"Because it is dangerous and invasive to augment Object.prototype (i.e., add instance methods to objects), all these methods are static methods that take an Object as their first parameter."* [Here's a prototype example using for/in.](http://jsfiddle.net/VunJy/) – user113716 Jan 12 '11 at 17:57
  • @patrick dw: Ah ok I didn't know that (could it be that they used to extend it?). Anyway if your code is used in an environment that you don't control, you *have to* use `hasOwnProperty` imo. – Felix Kling Jan 12 '11 at 17:59
  • @Felix Kling: Not sure if they used to. I think people get the impression that they do because they most certainly *do* extend Array, which people bump into when improperly using a `for/in` to iterate an Array. While I agree that in an environment that you *can't* control, you should use `hasOwnPoperty()`, my personal opinion is that in an environment that you *can* control, you should summarily remove/replace any code that does extend `Object.prototype` instead of slowing your code down with a function call. – user113716 Jan 12 '11 at 18:06
0

You can't do a lot without actually doing a check to see if the value is undefined and then doing operation a or operation b. It would be better to use a predicate to determine if the value is undefined:

x = $.grep(x, function(v, i) { return (typeof(v) != "undefined"); });
Matthew Abbott
  • 60,571
  • 9
  • 104
  • 129
-1

There isn't. The only way would be to omit the items from the collection completely, any solution you come up with would still have to do a test on each element for the value.

You could come up with different methods of adding the items key/value to object literals or what have you, but you would still need to omit undefined entries if you do not wish to enumerate over them.

Quintin Robinson
  • 81,193
  • 14
  • 123
  • 132