1

I wanted to write one version of a function that iterated over both Array objects and Objects, with minimal duplicate code. Something like:

function (arr_or_obj) {
   arr_or_obj.forEach( function(key,value) {
            // do stuff with key and value
       });
}

This was before I realized that (in Chrome at least), Object.keys returns a list of the keys for an array. So I could use that to treat the Array like an object. Like so:

function (arr_or_obj) {
   var keys = Object.keys(arr_or_obj);
   keys.forEach( function(key) {
            var value = arr_or_obj[key];
            // do stuff with key and value
       });
}

Problem solved. But this was not before I wrote my own "JavaScript pseudo-Array iterator". The only advantage of this was that instead of getting a list of keys for the Array (which could be very long), we save memory by producing only the return values we need. Like so:

    function create_iterator(max) {
            var jkeys = {};
            jkeys.count_ = 0;
            jkeys.length = max;
            jkeys.getter_ = function() {
                    var returnable = jkeys.count_;
                    jkeys.count_ += 1;
                    jkeys.__defineGetter__(jkeys.count_,jkeys.getter_);
                    delete jkeys[jkeys.count_-1];
                    return returnable;
            };
            jkeys.__defineGetter__(0, jkeys.getter_);
            return jkeys;
    }

Which you can then call by going:

var z = create_iterator(100);
z[0];
>> 0
z[0];
>> undefined
z[1];
>> 1;
z[2]
>> 2;
...

This is sort of a question and answer in one, but the obvious question is, is there a better way to do this without using Object.keys?

Scott
  • 21,211
  • 8
  • 65
  • 72

3 Answers3

1

Object.keys gives you an array of the object's own enumerable properties. That is, properties it has itself, not from its prototype, and that are enumerable (so not including things like length on arrays, which is a non-enumerable property).

You can do the same thing with for-in if you use correct safeguards:

var key;
for (key in arr_or_obj) {
    if (arr_or_obj.hasOwnProperty(key)) {
        // do something with it
    }
}

Now you're doing the same thing you were doing with Object.keys.

But, note that this (and Object.keys) will include properties people put on arrays that aren't array indexes (something which is perfectly valid in JavaScript). E.g.:

var key;
var a = [1, 2, 3];
a.foo = "bar";
for (key in a) {
    if (a.hasOwnProperty(key)) {
        console.log(key); // Will show "foo" as well as "0", "1", and "2"
    }
}

If you want to only process array indexes and not other properties, you'll need to know whether the thing is an array, and then if so use the "is this an index" test:

var key;
var isArray = Array.isArray(arr_or_obj);
// Or (see below):
//var isArray = Object.prototype.toString.call(arr_or_obj) === "[object Array]";
for (key in arr_or_obj) {
    if (a.hasOwnProperty(key) &&
        (!isArray || isArrayIndex(key))
       ) {
        console.log(key);   // Will only show "0", "1", and "2" for `a` above
    }
}

...where Array.isArray is a new feature of ES5 which is easily shimmed for older browsers if necessary:

if (!Array.isArray) {
    Array.isArray = (function() {
        var toString = Object.prototype.toString;
        function isArray(arg) {
            return toString.call(arg) === "[object Array]";
        }
        return isArray;
    })();
}

...and isArrayIndex is:

function isArrayIndex(key) {
    return /^0$|^[1-9]\d*$/.test(key) && key <= 4294967294; // 2^32 - 2
}

See this other answer for details on that (where the magic numbers come from, etc.).

That loop above will loop through an object's own enumerable properties (all of them) if it's not an array, and loop through the indexes (but not other properties) if it's an array.

Taking it a step further, what if you want to include the properties the object gets from its prototype? Object.keys omits those (by design). If you want to include them, just don't use hasOwnProperty in the for-in loop.

Community
  • 1
  • 1
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 1
    As always an impressive response, but maybe it would be better to use `Array.isArray(arr_or_obj);` instead of `Object.prototype.toString.call(arr_or_obj) === "[object Array]";` – Givi Jul 21 '13 at 09:22
  • 1
    @Givi: Good point, I'm allowing for older browsers and for people not using an ES5 shim. – T.J. Crowder Jul 21 '13 at 09:23
1

You can try something like this...
Live Demo

function forIn(obj) {
    var isArray = Array.isArray(obj),
        temp = isArray ? obj : Object.keys(obj);
    temp.forEach(function (value, index, array) {
        console.log(this[isArray ? index : value]);
    }, obj);
}
Givi
  • 1,674
  • 2
  • 20
  • 35
  • 1
    Cute! And +1. But I think the OP said they wanted to avoid the overhead of building the keys array if they could. – T.J. Crowder Jul 21 '13 at 09:31
0

You can write:

for (key in arr_or_obj) {
    if (arr_or_obj.hasOwnProperty(key) {
        value = arr_or_obj[key];
        // do stuff with key and value
    }
}
Barmar
  • 741,623
  • 53
  • 500
  • 612
  • It's a bad idea to use this to loop through arrays unless you take correct safeguards. More in [this answer](http://stackoverflow.com/questions/9329446/for-each-in-an-array-how-to-do-that-in-javascript/9329476#9329476). – T.J. Crowder Jul 21 '13 at 08:47
  • @T.J.Crowder That answer assumes that you know a priori that you're processing an array, so you can restrict the key values. He wants to treat arrays and objects the same. – Barmar Jul 21 '13 at 08:51
  • @ Barmar: I didn't say that answer answered this question, just that it explained (in the **Use for-in *correctly*** part) why using `for-in` on an array without safeguards is a bad idea unless you know what you're doing (e.g., you really do want to process non-index properties). – T.J. Crowder Jul 21 '13 at 08:55