Loops and Iteration
Walking a collection with for...of, repeating until a condition changes, break and continue, and the loop over object keys that hands you strings you did not expect.
Walking a collection with for...of, repeating until a condition changes, break and continue, and the loop over object keys that hands you strings you did not expect.
The arrays lesson gave you map and filter, which cover most
of what you do to a list. They do not cover everything. You
cannot stop map halfway, you cannot use it when the work
involves waiting, and you cannot use it at all to repeat
something until a condition changes.
By the end of this lesson you will know the loops worth writing, which to reach for, and the loop over object keys that hands you values you did not ask for — which is the one that produces a wrong answer rather than an error.
const photos = ["dawn.jpg", "tram.jpg", "square.jpg"];
for (const photo of photos) {
console.log(photo);
}for...of takes each element in turn. photo is a string,
inferred from the array — no annotation, no index arithmetic,
and nothing to get wrong.
It works on anything iterable, which includes strings, Maps
and Sets:
for (const character of "Lisbon") { ... }When you need the position too:
for (const [index, photo] of photos.entries()) {
console.log(`${index + 1}. ${photo}`);
}entries() yields a [index, value] tuple, destructured in the
loop header. That is better than tracking a counter yourself,
which is one more thing to get wrong.
There is a similar-looking loop with a very different meaning:
for (const index in photos) {
console.log(index); // "0", "1", "2" - STRINGS
}for...in walks keys, not values. On an array those keys
are the positions, as strings, which is almost never what you
want.
Bad — for...in over an array.
const sizes = [340, 128, 95];
let total = 0;
for (const i in sizes) {
total += sizes[i];
}Good — for...of over the
values.
const sizes = [340, 128, 95];
let total = 0;
for (const size of sizes) {
total += size;
}The first version happens to work, which is exactly why it survives review.
The values are strings, not numbers
i is "0", not 0. Using a string to index an array is
tolerated, so nothing complains — until you write
total += i and get "0340128".
It walks extra properties too
Anything anyone attached to the array, not only the positions.
The order is not guaranteed
For non-numeric keys the visiting order is unspecified, so the same code can behave differently elsewhere.
The rule is short: for...of for values, for...in for
object keys, and prefer Object.keys even then.
Object.entries gives key–value pairs, is typed, and does not
include anything inherited.
for...of needs a collection. Sometimes you only know the
condition to stop:
while checks first, so a false condition means the body never
runs. do...while runs once before checking:
And the counting for, which you will read more often than
write:
Three parts: set up, test before each pass, do after each pass.
It is the right tool when you need a number rather than a
collection — counting down, stepping by two — and for...of
with entries() is better whenever you have a collection.
The rule for choosing: for...of when you have a collection,
while when you have a condition. A while with a counter
you increment by hand is a for written the long way.
continue is the tidier alternative to wrapping the whole body
in an if. Handling the uninteresting cases first keeps the
real work at one level of indentation, which matters once a body
grows.
Both affect only the innermost loop. This is also the reason to
use a loop rather than forEach:
forEach cannot be stopped. If you need to leave early, use a
loop — or find, some and every, which stop as soon as the
answer is decided.
Both are correct, and the choice is not stylistic.
Producing a new collection or value.
They say what the result is rather than how it was built, they chain, and the types flow through with no annotations.
Stopping early, waiting, or side effects.
break genuinely leaves. await in the body genuinely
waits. And work done for its effect rather than its result
reads better as a loop.
That second case is the important one. Awaiting inside map
does not do what it looks like:
map does not wait, so it hands back promises and the code
after it runs before any upload completes. A for...of with
await inside genuinely goes one at a time; Promise.all runs
them together. The async lesson covers both properly — for now,
know that await inside map is a bug.
Everyone writes one:
Press Ctrl + C to stop a runaway program.
And the subtler version, changing a collection while walking it:
Removing an element shifts everything after it back one place, and the loop has already moved on — so the item that slid into the vacated position is never examined. There is no error; some corrupt photos just survive.
Build a new array instead:
You can now work through collections step by step, stop when you
have what you need, and you know the two loop bugs that produce
wrong answers rather than errors — for...in over an array, and
removing items while walking them.
Next is Working with Collections, which introduces two types built for jobs arrays do badly: looking things up by key, and answering "have I seen this before" without scanning. The performance note in the callout above is the reason.
Before you move on, write a loop that removes items from the
array it is walking, and watch it skip. Then rewrite it with
filter. That bug is invisible in review and obvious once you
have caused it on purpose.
THE LOOPS
for (const x of xs) VALUES - the default
for (const [i, x] of xs.entries()) value and position
for (const k in obj) KEYS - and prefer Object.keys/entries
while (condition) checks first
do { } while (condition) runs once, then checks
for (let i = 0; i < n; i++) when you need a number
for...of when you have a COLLECTION
while when you have a CONDITION
for...in ON AN ARRAY
gives "0", "1", "2" - strings, not values
walks extra properties, order not guaranteed
total += i silently produces "0340128"
use for...of
CONTROL
break leave the loop
continue skip to the next - handle the boring cases first
Ctrl+C stop a runaway program
forEach CANNOT be stopped - return skips one call only
need to leave early? a loop, or find/some/every
LOOP OR ARRAY METHOD
map/filter/reduce PRODUCING a new collection or value
a loop stopping early, awaiting, or side effects
photos.map(async p => await f(p)) <- a bug: does not wait
for (const p of photos) await f(p) <- one at a time
TRAPS
no counter change -> never ends
splice/remove while iterating -> items silently skipped
filter into a new array instead
a scan inside a loop -> use a Setfor (const [tag, count] of Object.entries(counts)) {
console.log(`${tag}: ${count}`);
}let attempts = 0;
while (attempts < 5 && !succeeded) {
succeeded = tryUpload();
attempts += 1;
}let input: string;
do {
input = prompt();
} while (input === "");for (let i = 0; i < 10; i += 1) { ... }for (const photo of photos) {
if (photo.name === target) {
found = photo;
break; // stop the loop
}
}for (const photo of photos) {
if (photo.isCorrupt) continue; // skip to the next
process(photo);
archive(photo);
}photos.forEach((photo) => {
if (photo.isCorrupt) return; // skips ONE call, not the loop
// there is no way to break out of forEach at all
});const names = photos.map((p) => p.name); // producing
for (const photo of photos) { // waiting
await upload(photo);
}photos.map(async (photo) => await upload(photo));
// an array of Promises; nothing has finishedlet count = 0;
while (count < 10) {
console.log(count);
// forgot: count += 1
}for (const photo of photos) {
if (photo.isCorrupt) {
photos.splice(photos.indexOf(photo), 1); // skips items
}
}const good = photos.filter((photo) => !photo.isCorrupt);for (const photo of photos) { // 400,000
if (processedNames.includes(photo.name)) continue; // scans
}