Checking if an object is array-like
As best I've found in my research on this topic, you have only a couple choices:
You can look only at the
.length
property and accept any object that seems to have an appropriate.length
property that isn't any other things you know you should eliminate (like a function).You can check for specific array-like objects (
HTMLCollection
,nodeList
) and bias in favor of them.
Here are two options for the first method - one that doesn't accept a zero length and one that does (these incorporate suggestions by gilly3 and things we see in jQuery's similar function):
// see if it looks and smells like an iterable object, but don't accept length === 0
function isArrayLike(item) {
return (
Array.isArray(item) ||
(!!item &&
typeof item === "object" &&
item.hasOwnProperty("length") &&
typeof item.length === "number" &&
item.length > 0 &&
(item.length - 1) in item
)
);
}
This, of course, reports false
for items with .length === 0
, If you want to allow .length === 0
, then the logic can be made to include that case too.
// see if it looks and smells like an iterable object, and do accept length === 0
function isArrayLike(item) {
return (
Array.isArray(item) ||
(!!item &&
typeof item === "object" &&
typeof (item.length) === "number" &&
(item.length === 0 ||
(item.length > 0 &&
(item.length - 1) in item)
)
)
);
}
Some test cases: http://jsfiddle.net/jfriend00/3brjc/
2) After checking to see that it's not an actual array, you can code to check for specific kinds of array-like objects (e.g. nodeList
, HTMLCollection
).
For example, here's a method I use when I want to make sure I include nodeList and HTMLCollection array-like objects:
// assumes Array.isArray or a polyfill is available
function canAccessAsArray(item) {
if (Array.isArray(item)) {
return true;
}
// modern browser such as IE9 / firefox / chrome etc.
var result = Object.prototype.toString.call(item);
if (result === "[object HTMLCollection]" || result === "[object NodeList]") {
return true;
}
//ie 6/7/8
if (typeof item !== "object" || !item.hasOwnProperty("length") || item.length < 0) {
return false;
}
// a false positive on an empty pseudo-array is OK because there won't be anything
// to iterate so we allow anything with .length === 0 to pass the test
if (item.length === 0) {
return true;
} else if (item[0] && item[0].nodeType) {
return true;
}
return false;
}
You can check if the object is iterable :
function isIterable(o){
return (o!=null && typeof(o[Symbol.iterator])==='function');
}
Careful though, returns true for strings. If that's a problem, exclude them :
function isIterable(o){
return (o!=null && typeof(o[Symbol.iterator])==='function' && typeof(o)!=='string');
}
Then either access the elements using the iterator or if you want to use the regular array[0] way, just add a check for length property.
Complete isArrayLike function :
function isArrayLike(a){
return (
a!=null &&
typeof(a[Symbol.iterator])==='function' &&
typeof(a.length)==='number' &&
typeof(a)!=='string'
);
}
Well, it depends what you mean by array-like. Probably something you could iterate with a for loop like this:
for (var i=0, l=obj.length; i<l; ++i) {
var item = obj[i];
}
So then the test is simple:
function isArrayLike(obj) {
if (!obj) return false;
var l = obj.length;
if (typeof l != 'number' || l < 0) return false;
if (Math.floor(l) != l) return false;
// fast check
if (l>0 && !((l-1) in obj)) return false;
// more complete check (optional)
for (var i=0; i<l; ++i) {
if (!(i in obj)) return false;
}
return true;
}
Of course, this won't catch arrays which are sparsely populated, but then again, are they really being used as arrays? NodeLists and the like won't be sparsely populated.
Enjoy!