Iterating arrays & array-likes
Choose the right loop for arrays, use keys, values and entries, convert array-likes with Array.from, and work safely with arguments and NodeList objects.
- 01Pick the right loopSay which of
for,for...of, andforEachcan skip an item, stop early, or give you the index. - 02Get indexes, values, or bothUse
keys()for indexes,values()for items, andentries()for both together. - 03Work with array-like valuesCopy
arguments, strings, array-like objects, and browserNodeLists into real arrays, or borrow one array method.
Pick the right way to go through a list
An array is a list of items in order. Going through the items one by one is called iterating. You do it all the time, but not always for the same reason:
- Sometimes you need each item's position, its index.
- Sometimes you need to stop as soon as you find a problem.
- Sometimes you want to do one thing with every item, like print it.
JavaScript has a different loop for each of these jobs. This lesson shows you which one to pick. It also shows you values that look like arrays but are not real arrays, and how to turn them into real ones.
It builds on these earlier lessons: Loops: while & for, for…of & for…in, Default & rest parameters, and Symbols. The exact rules for how loops get their values come in a later lesson. Here you use the parts you need today.
Iterating an array means visiting its items one at a time, in order. An array-like value has numbered slots (0, 1, 2, …) and a length, but it may not have array methods such as map or filter.
Picture a long row of school lockers. If you walk along them yourself with a counter, you can count by twos, walk backward, or stop at locker 12. If a guide walks with you, the guide hands you what is in each locker, in order. You can still say "stop" at any time.
A delivery driver works differently. The driver has a list and visits every house on it. The driver can skip one parcel, but can't end the whole trip from one doorstep.
- In real life: You walk along the lockers and count them yourself
- In JavaScript: A
forloop: you control the index, the direction, and when to stop - In real life: A guide hands you what is in each locker, in order
- In JavaScript: A
for...ofloop: you get each value in order, andbreakcan still stop it - In real life: A delivery driver visits every house on the list
- In JavaScript:
forEach: your function runs for each item, and you can't stop it early
Here is the plan for the lesson:
- Do the same job with three different loops.
- Use three small helpers that give you indexes, values, or both.
- Turn array-like values into real arrays.
- Borrow an array method for one call.
- Look at a real
NodeListon this page.
for, for…of, and forEach on the same job
STEP THROUGHThe job is small: add up the scores until a negative number appears, and skip any zero. The scores are [3, 0, 4, -2, 5]. The right answer is 7. That's 3 + 4. The 5 comes after -2, so it should never be added.
Two keywords control a loop from the inside:
continueskips the rest of this turn and goes to the next item.breakends the whole loop.
forEach is an array method. You give it a function, and it calls that function once for each item. A function you hand to another function like this is called a callback. Inside a callback, return ends only that one call.
The same scores, three loops. Guess whether the 5 after -2 gets added, then step through the real run.
script
let total = 0;for (let index = 0; index < scores.length; index += 1) { const score = scores[index]; if (score === 0) continue; if (score < 0) break; total += score;}console.log(total);for...of, break ends the whole loop at -2. A for loop stops the same way. In forEach, return skips only -2, so the walk goes on to 5.No loop is always the best. Each one gives you something different:
forgives you a counter, which is the index. Use it when the index matters most, when you want to skip or go backward, or when you want full control.for...ofgives you each value. Use it when you care about the values and may needbreakorcontinue.forEachcalls your callback for each item. Use it when you want to do something with every item, such as printing it. Don't use it when you may need to stop early.
The table compares the loops. Read each row as: "if I use this loop, can it stop, does it give me the index, and what is it good for?" A hole, in the fourth column, is an empty slot in an array. You'll meet holes in the next section.
| Loop | Can it stop early? | Does it give the index? | What happens at a hole? | Use it when |
|---|---|---|---|---|
for | Yes, with break. | Yes. The counter is the index. | It reads every index. A hole reads as undefined. | You want full control: where to start, where to stop, skipping, or going backward. |
for...of | Yes, with break. | No. Use entries() to get it. | It visits a hole and gets undefined. | You want each value in order and may need to stop early. |
forEach | No. return skips only one item. | Yes. It is the second input to your callback. | It skips holes. | You want to do the same thing with every item. |
for...in | Yes, with break. | It gives property names as text, like "0". | It skips holes, but it also shows extra properties you added. | You loop over an object's properties, not an array. |
The last row is for...in. It is easy to mix up with for...of, so it is here as a warning. On an array, for...in gives you the property names as text, like "0" and "1". It also gives you any extra properties you added to the array. Use for...in for objects. For arrays, use one of the first three.
forEach has the same problem with work that finishes later. Code that finishes later is called async. An async function gives back a promise: an object that says "the result will be ready later". forEach doesn't wait for those promises. It finishes straight away and gives back undefined.
const orders = ["tea", "toast"];
orders.forEach(async (order) => {
await Promise.resolve();
console.log("done " + order);
});
console.log("forEach returned");Line 6 prints forEach returned first. Only after that does each callback continue past await Promise.resolve() on line 3. Then line 4 prints done tea and done toast.
If the order matters, use for...of with await inside it. If the order doesn't matter, collect the promises in an array and wait for all of them together.
Changing the array while you loop over it is another trap. When you remove an item, every item after it moves one place to the left. But the for loop still moves its index one place to the right. So it can jump over an item.
const cart = ["tea", "milk", "milk", "bread"];
for (let index = 0; index < cart.length; index += 1) {
if (cart[index] === "milk") cart.splice(index, 1);
}
console.log(cart.join(","));splice(index, 1) removes one item at index. Line 3 removes the first milk at index 1. The second milk moves into index 1. Then the loop moves on to index 2, so it never checks the second milk. Line 5 prints tea,milk,bread.
If you want to remove items, the later map, filter & reduce lesson shows a clearer way: filter builds a new array and leaves the old one alone.
keys(), values(), and entries()
STEP THROUGHArrays have three small helper methods:
keys()gives the indexes.values()gives the items.entries()gives pairs:[index, value].
They don't give you an array. Each one gives you an iterator: an object that hands out one result at a time to a loop. To see all the results at once, copy them into an array. You can spread them with ... inside [ ], or use Array.from.
keys() gives the positions. values() gives the items. entries() gives both together, as a small [index, value] pair.Step through the three helpers: keys() for indexes, values() for items, entries() for both.
script
console.log([...queue.keys()].join(","));console.log([...queue.values()].join("|"));for (const [index, name] of queue.entries()) { console.log(index + ":" + name);}You saw entries() in the for…of lesson. This pattern is worth keeping:
const names = ["Ada", "Ben"];
for (const [index, name] of names.entries()) {
console.log(index, name);
}Line 2 gets one pair at a time. The [index, name] part splits each pair into two variables. This is called destructuring. Line 3 prints 0 Ada, then 1 Ben. You don't have to write names[index], and you can still use break if you need it.
Now an unusual case. An array can have an empty slot, called a hole. In ["hat", , "coat"], the two commas with nothing between them leave index 1 empty. An array with holes is called a sparse array.
Holes are rare, but they explain some strange output. Step through to see that for...of visits the hole and forEach skips it.
script
for (const item of lockers) { console.log(item);}lockers.forEach((item) => { console.log(item);});You won't see holes often, but they explain some strange output. for...of visits every position. At the hole it gets undefined. Most array methods that take a callback, like forEach, skip holes. Their callback never runs for that slot.
Array-like values and Array.from
INTERACTIVEAn array-like value has numbered slots and a length. It looks like an array, but it doesn't get array methods. For example, { 0: "a", 1: "b", length: 2 } is a plain object that is array-like. Array.isArray checks for a real array, and for this object it gives false.
You have notes stuck on the fridge in spots 1 and 2. You know where each one is, but you can't sort them or cross them off. So you copy each note into a shopping list app. Now the list can do all the things the app knows how to do.
- In real life: Notes stuck in numbered places on the fridge
- In JavaScript: An array-like: slots
0,1, and alength - In real life: A shopping list app that can sort and cross off
- In JavaScript: Array methods such as
map,filter,join, andslice - In real life: Copy each note into the app
- In JavaScript:
Array.from(arrayLike)gives back a real array
row has numbered slots and a length, but it is a plain object, so it has no map. Array.from reads slots 0 and 1 and copies them into a new, real array.An array-like value looks like an array but has no array methods. Step through copying it, then borrowing a method.
script
console.log(Array.isArray(row));try { row.map((letter) => letter.toUpperCase());} catch (error) { console.log(error.name);}const real = Array.from(row);console.log(real.map((letter) => letter.toUpperCase()).join(""));console.log(Array.prototype.map.call(row, (letter) => letter + "!").join("|"));console.log(Array.prototype.join.call("abc", "-"));Array.from accepts two kinds of values:
- Array-like values, with numbered slots and a
length. - Iterable values: values that
for...ofcan loop over.
A string is both. It has numbered characters and a length, and for...of works on it. A Set is iterable but not array-like. It has no numbered slots and no length.
["a", "b"]- the
argumentsobject inside a regular function "abc"{ 0: "a", 1: "b", length: 2 }- document.querySelectorAll(
section) - new Set([
a,b]) { name: "Ada" }
Sort each value. First ask: is it a real array? If not, does it have numbered slots and a length?
Borrow one array method for an array-like
PRACTICALSometimes you need just one array method, one time. You don't need to keep a new array. Then you can borrow the method. All array methods live on Array.prototype, the shared object every array gets its methods from.
You borrow a method with .call(). It runs a function and lets you choose what this is inside it. So Array.prototype.map.call(row, fn) runs map with row as this. map then reads the slots of row. The later call, apply & bind lesson explains .call() in full.
const row = { 0: "a", 1: "b", length: 2 };
const excited = Array.prototype.map.call(row, (letter) => letter + "!");
console.log(excited.join("|"));
console.log(Array.prototype.join.call("abc", "-"));Line 2 borrows map and adds ! to each letter. Line 3 prints a!|b!. Line 5 borrows join for the string "abc" and prints a-b-c.
Borrowing works, but copying first is usually easier to read: Array.from(row).map(fn). Here is how to choose:
- Borrow when you need one array method once, for one array-like value.
- Copy with
Array.fromwhen you will use several array methods or pass the array on.
If a teammate has to stop and think about Array.prototype.map.call, then Array.from(row).map is the better choice. The small speed difference almost never matters in a normal app.
Two array-likes you will meet: arguments and NodeList
LIVE BROWSERTwo array-like values show up often in real code:
arguments, which holds the values passed into a regularfunction.- A
NodeList, which the browser gives back from methods such asquerySelectorAll.
You met arguments in Default & rest parameters. When you write the function yourself, use a rest parameter instead. It gives you a real array straight away.
function list() {
console.log(Array.isArray(arguments));
console.log([...arguments].join("|"));
console.log(Array.from(arguments).map((value) => value * 2).join(","));
}
list(2, 4, 6);Line 2 prints false: arguments is not a real array, so it has no map. Line 3 spreads it into a new array with [...arguments], which works in today's browsers. Line 4 copies it with Array.from and then uses map. A rest parameter is simpler: function list(...values) { ... } gives you a real array called values.
A web page is made of parts, such as headings, buttons, and sections. The browser lets your code reach these parts through the DOM, short for Document Object Model. A later lesson, Walking & searching the DOM, covers it in full. For now, one fact matters: querySelectorAll gives back a NodeList, not an array.
querySelectorAll collects the matching parts of the page into a NodeList. It has numbered slots, a length, and forEach, but no map.const sections = document.querySelectorAll("section");
console.log(sections.length);
console.log(typeof sections.forEach);
console.log(typeof sections.map);
const headings = Array.from(sections, (section) => section.id);
console.log(headings.join(", "));const sections = document.querySelectorAll("section");console.log(sections.length);console.log(typeof sections.forEach);console.log(typeof sections.map);const headings = Array.from(sections, (section) => section.id);console.log(headings.join(", "));- sections.length
- 0
- typeof sections.forEach
- not checked yet
- typeof sections.map
- not checked yet
- section ids
- Press the button to read this page.
Press the button. Your browser will collect every section on this page.
length, usually forEach, and no map.In most browsers today, a NodeList has forEach but not map. That's why real code often copies it first: Array.from(document.querySelectorAll("button")). After that, you can use map, filter, or sort.
Where you will use this
You pick a loop every time you work with a list of data in an app. Some common cases:
- Checking form fields until the first error: use
for...of, because it can stop withbreak. - Showing which field has the error: use
entries(), so you have the index as well as the value. - Working with a list the browser gives you: use
Array.fromto get a real array.
const fields = ["name", "email", "password"];
const values = ["Ada", "", "secret"];
for (const [index, value] of values.entries()) {
if (value === "") {
console.log(fields[index] + " is required");
break;
}
}Line 4 goes through the values with their indexes. Line 5 checks for an empty value. The email is empty, so line 6 uses its index to print email is required. Line 7 stops the loop, so you see only the first error.
Common mistakes
- "return stops forEach." It ends only the current callback call.
forEachstill calls the callback for the rest of the items. - "Array-like means array." Array-like only means numbered slots and a
length.Array.isArraycan still givefalse. - "A NodeList has every array method because it has forEach." Most browsers give it
forEach. Methods such asmap,filter, andreduceare only on real arrays. - "for...in is the index loop for arrays." It loops over property names. It gives them as text, and it also shows extra properties you added.
- "A hole is the same as an undefined value."
for...ofvisits a hole and getsundefined.forEachskips it. - "Borrowing is always better than copying." Borrowing is short but harder to read.
Array.fromis often clearer, especially before you use several array methods.
Practice exercises
5 EXERCISESreturn does inside forEachRead the code. Type the exact number it prints.
const scores = [3, 0, 4, -2, 5];
let total = 0;
scores.forEach((score) => {
if (score === 0) return;
if (score < 0) return;
total += score;
});
console.log(total);It prints 12. It adds 3, skips 0, adds 4, returns early for -2, and then still adds 5.
Run the code, or work it out in your head. It copies the row into an array, makes each letter a capital, and prints the letters joined together. What does it print?
const row = { 0: "x", 1: "y", length: 2 };
const result = Array.from(row).map((letter) => letter.toUpperCase());
console.log(result.join(""));const row = { 0: "x", 1: "y", length: 2 };
const result = Array.from(row).map((letter) => letter.toUpperCase());
console.log(result.join(""));Array.from(row) makes ["x", "y"]. map turns each letter into a capital letter. join("") puts them together, so it prints XY.
entries()What is the second line this loop prints?
const pets = ["cat", "dog"];
for (const [index, pet] of pets.entries()) {
console.log(index + ":" + pet);
}const pets = ["cat", "dog"];
for (const [index, pet] of pets.entries()) {
console.log(index + ":" + pet);
}The loop prints 0:cat, then 1:dog. Destructuring splits each pair into index and pet.
Open the browser console and run the NodeList code from this lesson. Which array method does the NodeList not have?
const sections = document.querySelectorAll("section");
console.log(sections.length);
console.log(typeof sections.forEach);
console.log(typeof sections.map);
const headings = Array.from(sections, (section) => section.id);
console.log(headings.join(", "));A NodeList from querySelectorAll has length and usually forEach, but it has no map. Copy it with Array.from(sections) first.
join from arraysWhat does this borrowed method call print?
console.log(Array.prototype.join.call("abc", "-"));console.log(Array.prototype.join.call("abc", "-"));Array.prototype.join.call("abc", "-") reads the string like an array-like value, so it prints a-b-c.
Check your understanding
8 QUESTIONSQuestion 1 of 8You need to stop as soon as a negative number appears. Which loop should you pick?
Choose an answer to see the explanation.
Question 2 of 8What does this
for...ofprogram print?Read the code, then predictconst values = [1, 0, 2, -1, 9]; let total = 0; for (const value of values) { if (value === 0) continue; if (value < 0) break; total += value; } console.log(total);Choose an answer to see the explanation.
Question 3 of 8This array has a hole. How many times does the
forEachcallback run?Read the code, then predictconst sparse = ["a", , "b"]; let count = 0; sparse.forEach(() => { count += 1; }); console.log(count);Choose an answer to see the explanation.
Question 4 of 8With
const names = ["Ada"], which code gives the first result[0, "Ada"]? (.next().valueasks an iterator for its next result.)Choose an answer to see the explanation.
Question 5 of 8What does
Array.from(row).join("-")print?Read the code, then predictconst row = { 0: "a", 1: "b", length: 2 }; console.log(Array.from(row).join("-"));Choose an answer to see the explanation.
Question 6 of 8What does the borrowed
joinprint for the string"abc"?Read the code, then predictconsole.log(Array.prototype.join.call("abc", "-"));Choose an answer to see the explanation.
Question 7 of 8Inside a regular function in today's JavaScript, what is true about
arguments?Choose an answer to see the explanation.
Question 8 of 8A browser
NodeListfromquerySelectorAllusually has which of these?Choose an answer to see the explanation.
Key takeaways
- Use
forwhen you need control over the index. - Use
for...ofto go through values in a way that is easy to read. It can stillbreakorcontinue. - Use
forEachto do something with every item. It can't stop early, and it doesn't build a new array. keys(),values(), andentries()give iterators. Spread them or useArray.fromto see all the results.- Array-like values have numbered slots and a
length. Copy them withArray.fromwhen you want array methods.
Iterating means picking the right way to visit each item. For an array-like value, either copy it into a real array or borrow one method.
Up next: Destructuring arrays & objects.