How can I loop through all the entries in an array using JavaScript?
I thought it was something like this:
forEach(instance in theArray)
Where theArray
is my array, but this seems to be incorrect.
Answer
TL;DR
- Don't use
for-in
unless you use it with safeguards or are at least aware of why it might bite you. Your best bets are usually
But there's lots more to explore, read on...
JavaScript has powerful semantics for looping through arrays and array-like objects. I've split the answer into two parts: Options for genuine arrays, and options for things that are just array-like, such as the arguments
object, other iterable objects (ES2015+), DOM collections, and so on.
I'll quickly note that you can use the ES2015 options now, even on ES5 engines, by transpiling ES2015 to ES5. Search for "ES2015 transpiling" / "ES6 transpiling" for more...
Okay, let's look at our options:
For Actual Arrays
You have three options in ECMAScript 5 ("ES5"), the version most broadly supported at the moment, and two more added in ECMAScript 2015 ("ES2015", "ES6"):
- Use
forEach
and related (ES5+) - Use a simple
for
loop - Use
for-in
correctly - Use
for-of
(use an iterator implicitly) (ES2015+) - Use an iterator explicitly (ES2015+)
Details:
1. Use forEach
and related
In any vaguely-modern environment (so, not IE8) where you have access to the Array
features added by ES5 (directly or using polyfills), you can use forEach
(spec
| MDN
):
var a = ["a", "b", "c"];
a.forEach(function(entry) {
console.log(entry);
});
forEach
accepts a callback function and, optionally, a value to use as this
when calling that callback (not used above). The callback is called for each entry in the array, in order, skipping non-existent entries in sparse arrays. Although I only used one argument above, the callback is called with three: The value of each entry, the index of that entry, and a reference to the array you're iterating over (in case your function doesn't already have it handy).
Unless you're supporting obsolete browsers like IE8 (which NetApps shows at just over 4% market share as of this writing in September 2016), you can happily use forEach
in a general-purpose web page without a shim. If you do need to support obsolete browsers, shimming/polyfilling forEach
is easily done (search for "es5 shim" for several options).
forEach
has the benefit that you don't have to declare indexing and value variables in the containing scope, as they're supplied as arguments to the iteration function, and so nicely scoped to just that iteration.
If you're worried about the runtime cost of making a function call for each array entry, don't be; details.
Additionally, forEach
is the "loop through them all" function, but ES5 defined several other useful "work your way through the array and do things" functions, including:
every
(stops looping the first time the callback returnsfalse
or something falsey)some
(stops looping the first time the callback returnstrue
or something truthy)filter
(creates a new array including elements where the filter function returnstrue
and omitting the ones where it returnsfalse
)map
(creates a new array from the values returned by the callback)reduce
(builds up a value by repeatedly calling the callback, passing in previous values; see the spec for the details; useful for summing the contents of an array and many other things)reduceRight
(likereduce
, but works in descending rather than ascending order)
2. Use a simple for
loop
Sometimes the old ways are the best:
var index;
var a = ["a", "b", "c"];
for (index = 0; index < a.length; ++index) {
console.log(a[index]);
}
If the length of the array won't change during the loop, and it's in performance-sensitive code (unlikely), a slightly more complicated version grabbing the length up front might be a tiny bit faster:
var index, len;
var a = ["a", "b", "c"];
for (index = 0, len = a.length; index < len; ++index) {
console.log(a[index]);
}
And/or counting backward:
var index;
var a = ["a", "b", "c"];
for (index = a.length - 1; index >= 0; --index) {
console.log(a[index]);
}
But with modern JavaScript engines, it's rare you need to eke out that last bit of juice.
In ES2015 and higher, you can make your index and value variables local to the for
loop:
let a = ["a", "b", "c"];
for (let index = 0; index < a.length; ++index) {
let value = a[index];
console.log(index, value);
}
//console.log(index); // would cause "ReferenceError: index is not defined"
//console.log(value); // would cause "ReferenceError: value is not defined"
let a = ["a", "b", "c"];
for (let index = 0; index < a.length; ++index) {
let value = a[index];
console.log(index, value);
}
try {
console.log(index);
} catch (e) {
console.error(e); // "ReferenceError: index is not defined"
}
try {
console.log(value);
} catch (e) {
console.error(e); // "ReferenceError: value is not defined"
}
And when you do that, not just value
but also index
is recreated for each loop iteration, meaning closures created in the loop body keep a reference to the index
(and value
) created for that specific iteration:
let divs = document.querySelectorAll("div");
for (let index = 0; index < divs.length; ++index) {
divs[index].addEventListener('click', e => {
console.log("Index is: " + index);
});
}
let divs = document.querySelectorAll("div");
for (let index = 0; index < divs.length; ++index) {
divs[index].addEventListener('click', e => {
console.log("Index is: " + index);
});
}
zero
one
two
three
four
If you had five divs, you'd get "Index is: 0" if you clicked the first and "Index is: 4" if you clicked the last. This does not work if you use var
instead of let
.
3. Use for-in
correctly
You'll get people telling you to use for-in
, but that's not what for-in
is for. for-in
loops through the enumerable properties of an object, not the indexes of an array. The order is not guaranteed, not even in ES2015 (ES6). ES2015+ does define an order to object properties (via [[OwnPropertyKeys]]
, [[Enumerate]]
, and things that use them like Object.getOwnPropertyKeys
), but it does not define that for-in
will follow that order. (Details in this other answer.)
The only real use cases for for-in
on an array are:
- It's a sparse arrays with massive gaps in it, or
- You're using non-element properties and you want to include them in the loop
Looking only at that first example: You can use for-in
to visit those sparese array elements if you use appropriate safeguards:
// `a` is a sparse array
var key;
var a = [];
a[0] = "a";
a[10] = "b";
a[10000] = "c";
for (key in a) {
if (a.hasOwnProperty(key) && // These checks are
/^0$|^[1-9]\d*$/.test(key) && // explained
key <= 4294967294 // below
) {
console.log(a[key]);
}
}
Note the three checks:
That the object has its own property by that name (not one it inherits from its prototype), and
That the key is all decimal digits (e.g., normal string form, not scientific notation), and
That the key's value when coerced to a number is <= 2^32 - 2 (which is 4,294,967,294). Where does that number come from? It's part of the definition of an array index in the specification. Other numbers (non-integers, negative numbers, numbers greater than 2^32 - 2) are not array indexes. The reason it's 2^32 - 2 is that that makes the greatest index value one lower than 2^32 - 1, which is the maximum value an array's
length
can have. (E.g., an array's length fits in a 32-bit unsigned integer.) (Props to RobG for pointing out in a comment on my blog post that my previous test wasn't quite right.)
You wouldn't do that in inline code, of course. You'd write a utility function. Perhaps:
// Utility function for antiquated environments without `forEach`
var hasOwn = Object.prototype.hasOwnProperty;
var rexNum = /^0$|^[1-9]\d*$/;
function sparseEach(array, callback, thisArg) {
var index;
for (var key in array) {
index = +key;
if (hasOwn.call(a, key) &&
rexNum.test(key) &&
index <= 4294967294
) {
callback.call(thisArg, array[key], index, array);
}
}
}
var a = [];
a[5] = "five";
a[10] = "ten";
a[100000] = "one hundred thousand";
a.b = "bee";
sparseEach(a, function(value, index) {
console.log("Value at " + index + " is " + value);
});
4. Use for-of
(use an iterator implicitly) (ES2015+)
ES2015 adds iterators to JavaScript. The easiest way to use iterators is the new for-of
statement. It looks like this:
const a = ["a", "b", "c"];
for (const val of a) {
console.log(val);
}
Under the covers, that gets an iterator from the array and loops through it, getting the values from it. This doesn't have the issue that using for-in
has, because it uses an iterator defined by the object (the array), and arrays define that their iterators iterate through their entries (not their properties). Unlike for-in
in ES5, the order in which the entries are visited is the numeric order of their indexes.
5. Use an iterator explicitly (ES2015+)
Sometimes, you might want to use an iterator explicitly. You can do that, too, although it's a lot clunkier than for-of
. It looks like this:
const a = ["a", "b", "c"];
const it = a.values();
let entry;
while (!(entry = it.next()).done) {
console.log(entry.value);
}
The iterator is an object matching the Iterator definition in the specification. Its next
method returns a new result object each time you call it. The result object has a property, done
, telling us whether it's done, and a property value
with the value for that iteration. (done
is optional if it would be false
, value
is optional if it would be undefined
.)
The meaning of value
varies depending on the iterator; arrays support (at least) three functions that return iterators:
values()
: This is the one I used above. It returns an iterator where eachvalue
is the array entry for that iteration ("a"
,"b"
, and"c"
in the example earlier).keys()
: Returns an iterator where eachvalue
is the key for that iteration (so for oura
above, that would be"0"
, then"1"
, then"2"
).entries()
: Returns an iterator where eachvalue
is an array in the form[key, value]
for that iteration.
For Array-Like Objects
Aside from true arrays, there are also array-like objects that have a length
property and properties with numeric names: NodeList
instances, the arguments
object, etc. How do we loop through their contents?
Use any of the options above for arrays
At least some, and possibly most or even all, of the array approaches above frequently apply equally well to array-like objects:
Use
forEach
and related (ES5+)The various functions on
Array.prototype
are "intentionally generic" and can usually be used on array-like objects viaFunction#call
orFunction#apply
. (See the Caveat for host-provided objects at the end of this answer, but it's a rare issue.)Suppose you wanted to use
forEach
on aNode
'schildNodes
property. You'd do this:Array.prototype.forEach.call(node.childNodes, function(child) {
// Do something with `child`
});If you're going to do that a lot, you might want to grab a copy of the function reference into a variable for reuse, e.g.:
// (This is all presumably in some scoping function)
var forEach = Array.prototype.forEach;
// Then later...
forEach.call(node.childNodes, function(child) {
// Do something with `child`
});Use a simple
for
loopObviously, a simple
for
loop applies to array-like objects.Use
for-in
correctlyfor-in
with the same safeguards as with an array should work with array-like objects as well; the caveat for host-provided objects on #1 above may apply.Use
for-of
(use an iterator implicitly) (ES2015+)for-of
will use the iterator provided by the object (if any); we'll have to see how this plays with the various array-like objects, particularly host-provided ones. For instance, the specification for theNodeList
fromquerySelectorAll
was updated to support iteration. The spec for theHTMLCollection
fromgetElementsByTagName
was not.Use an iterator explicitly (ES2015+)
See #4, we'll have to see how iterators play out.
Create a true array
Other times, you may want to convert an array-like object into a true array. Doing that is surprisingly easy:
Use the
slice
method of arraysWe can use the
slice
method of arrays, which like the other methods mentioned above is "intentionally generic" and so can be used with array-like objects, like this:var trueArray = Array.prototype.slice.call(arrayLikeObject);
So for instance, if we want to convert a
NodeList
into a true array, we could do this:var divs = Array.prototype.slice.call(document.querySelectorAll("div"));
See the Caveat for host-provided objects below. In particular, note that this will fail in IE8 and earlier, which don't let you use host-provided objects as
this
like that.It's also possible to use ES2015's spread syntax with JavaScript engines that support this feature:
var trueArray = [...iterableObject];
So for instance, if we want to convert a
NodeList
into a true array, with spread syntax this becomes quite succinct:var divs = [...document.querySelectorAll("div")];
Array.from
(ES2015+, but easily polyfilled) creates an array from an array-like object, optionally passing the entries through a mapping function first. So:var divs = Array.from(document.querySelectorAll("div"));
Or if you wanted to get an array of the tag names of the elements with a given class, you'd use the mapping function:
// Arrow function (ES2015):
var divs = Array.from(document.querySelectorAll(".some-class"), element => element.tagName);
// Standard function (since `Array.from` can be shimmed):
var divs = Array.from(document.querySelectorAll(".some-class"), function(element) {
return element.tagName;
});
Caveat for host-provided objects
If you use Array.prototype
functions with host-provided array-like objects (DOM lists and other things provided by the browser rather than the JavaScript engine), you need to be sure to test in your target environments to make sure the host-provided object behaves properly. Most do behave properly (now), but it's important to test. The reason is that most of the Array.prototype
methods you're likely to want to use rely on the host-provided object giving an honest answer to the abstract [[HasProperty]]
operation. As of this writing, browsers do a very good job of this, but the 5.1 spec did allow for the possibility a host-provided object may not be honest. It's in §8.6.2, several paragraphs below the big table near the beginning of that section), where it says:
Host objects may implement these internal methods in any manner unless specified otherwise; for example, one possibility is that
[[Get]]
and[[Put]]
for a particular host object indeed fetch and store property values but[[HasProperty]]
always generates false.
(I couldn't find the equivalent verbiage in the ES2015 spec, but it's bound to still be the case.) Again, as of this writing the common host-provided array-like objects in modern browsers [NodeList
instances, for instance] do handle [[HasProperty]]
correctly, but it's important to test.)
No comments:
Post a Comment