Chapter 8
Loops: Doing It Again Until It's Done
while, for, for...of, and the loop you'll write by accident.
So far every line you write runs once. If you want something ten times you type it ten times, which you already know is not how this is supposed to work.
while
while repeats a block for as long as its condition holds. Same
parentheses and braces as if, and it re-checks the condition every time
round:
let count = 1;
while (count <= 3) {
console.log(`while ${count}`);
count++;
}
while 1
while 2
while 3
Three things have to be true for that to work, and every loop you write needs all three:
- A starting point.
countis 1 before the loop begins. - A condition that will eventually be false.
count <= 3. - Something inside that moves toward it.
count++.
Miss the third and the loop runs forever. That’s not a hypothetical.
for
When you’re counting, all three parts belong together, and for puts them
on one line:
for (let i = 1; i <= 3; i++) {
console.log(`for ${i}`);
}
for 1
for 2
for 3
Three pieces separated by semicolons: start, keep going while, and
do after each round. Exactly the same three things the while needed,
just gathered where you can see them all at once, which is why a missing
i++ is much harder to write by accident here.
i is the traditional name for a loop counter. It’s one of the few places
a one-letter name is genuinely fine.
for…of
Most of the time you don’t care about the counting, you just want each
item in turn. That’s for...of:
for (const letter of "Ada") {
console.log(letter);
}
A
d
a
No counter, no condition, no chance of an off-by-one. It works on strings now and on arrays in chapter 10, and it’s the loop you’ll reach for most often once you have both.
Note const letter, not let. Each time round is a fresh letter, so
nothing is being reassigned and const is honest.
Stopping early, and skipping one
break leaves the loop immediately:
for (let i = 1; i <= 5; i++) {
if (i === 3) break;
console.log(`break-demo ${i}`);
}
break-demo 1
break-demo 2
continue skips the rest of this round and starts the next:
for (let i = 1; i <= 5; i++) {
if (i % 2 === 0) continue;
console.log(`continue-demo ${i}`);
}
continue-demo 1
continue-demo 3
continue-demo 5
continue is how you filter without nesting. The alternative is wrapping
the whole body in an if, which pushes everything one level deeper for no
gain.
Keeping something between rounds
A loop that only prints is the simple case. More often you want an answer at the end, which means a variable that lives outside the loop and gets updated inside it:
let total = 0;
for (const price of [3, 5, 2]) {
total += price;
}
console.log(total);
10
total has to be declared before the loop, because a variable declared
inside would be brand new every round and thrown away at the end of it.
That’s the whole pattern: start with an answer that’s true for nothing at
all, then improve it once per item.
Zero is the right starting point for a sum. For other jobs it’s a different starting value, but the shape doesn’t change:
let longest = "";
for (const word of ["hi", "hello", "hey"]) {
if (word.length > longest.length) longest = word;
}
console.log(longest);
hello
Counting is the same idea with ++, and "how many a's are in this word" is three lines you can now write without thinking about it. Chapter
14 has tools that do these in one line, and they’ll make a lot more sense
for having written the loop first.
Paying off chapter 5
Chapter 5 mentioned a phantom empty string at the end of split input, and said it would matter once loops arrived. It has arrived:
const lines = "a\nb\n".split("\n");
console.log(lines);
for (const line of lines) {
console.log(`line [${line}]`);
}
[ 'a', 'b', '' ]
line [a]
line [b]
line []
Two lines of input, three times round the loop. The trailing newline from
the last answer leaves an empty string behind, and asking for lines[0]
and lines[1] by hand hid that completely. A loop can’t hide it, because
a loop takes everything.
The fix is one line, and it’s why continue was worth introducing:
for (const line of lines) {
if (line === "") continue;
console.log(`line [${line}]`);
}
line [a]
line [b]
Exercise 1 · Count the lines
Write a program that reads input, splits it, and prints how many lines it got, both with and without skipping empty ones. Feed it three lines and watch one version confidently report four.
This is the difference between a number that’s right and a number that looks right, and it’s worth meeting on a program where you know the answer.
Check yourself
Project
The number guessing game
Roughly 45 minutes
A secret number, and guesses that get told higher or lower until one is right. This is the first program in the book that behaves like a game.
printf '50\n25\n42\n' | node guess.js50 is too high.
25 is too low.
42 is right! Took 3 guesses.The shape of it:
- A
constfor the secret number at the top. - Read the input and
.split("\n")it, exactly as chapter 5 did. for...ofover the guesses, skipping empty lines withcontinue.- Count the attempts as you go.
if/else if/elsefor too low, too high, correct.breakwhen they get it, so later guesses aren’t judged.- After the loop, say something if they never got it.
Rules: one read, one loop, and changing only the secret at the top should still work correctly.
Test the ending you’ll forget. Feed it guesses that never win:
printf '1\n2\n' | node guess.js1 is too low.
2 is too low.
Ran out of guesses.A loop that finishes normally and a loop that hits break are two
different endings, and only one of them means they won. Tracking that
takes a boolean you set before the break, which is chapter 3 and
chapter 7 doing quiet work together.
Stretch: make the secret random with
Math.floor(Math.random() * 100) + 1. The game gets genuinely
unwinnable-by-printf, which is a fair trade for it being a real game,
and it’s a good reminder that a program you can’t test the same way
twice is harder to trust.