O'Reilly logo

Learning JavaScript, 3rd Edition by Todd Brown

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Control Flow

A common metaphor given to beginning programmers is following a recipe. That metaphor can be useful, but it has an unfortunate weakness: to achieve repeatable results in the kitchen, one has to minimize choices. Once a recipe is refined, the idea is that it is followed, step by step, with little to no variation. Occasionally, of course, there will be choices: “substitute butter for lard,” or “season to taste,” but a recipe is primarily a list of steps to be followed in order.

This chapter is all about change and choice: giving your program the ability to respond to changing conditions and intelligently automate repetitive tasks.

Note

If you already have programming experience, especially in a language with a syntax inherited from C (C++, Java, C#), and are comfortable with control flow statements, you can safely skim or skip the first part of this chapter. If you do, however, you won’t learn anything about the gambling habits of 19th-century sailors.

A Control Flow Primer

Chances are, you’ve been exposed to the concept of a flowchart, which is a visual way of representing control flow. As our running example in this chapter, we’re going to write a simulation. Specifically, we are going to simulate a Midshipman in the Royal Navy in the mid-19th century playing Crown and Anchor, a betting game popular at the time.

The game is simple: there’s a mat with six squares with symbols for “Crown,” “Anchor,” “Heart,” “Club,” “Spade,” and “Diamond.” The sailor places any number of coins on any combination of the squares: these become the bets. Then he1 rolls three six-sided dice with faces that match the squares on the mat. For each die that matches a square that has a bet on it, the sailor wins that amount of money. Here are some examples of how the sailor might play, and what the payout is:

Bet Roll Payout

5 pence on Crown

Crown, Crown, Crown

15 pence

5 pence on Crown

Crown, Crown, Anchor

10 pence

5 pence on Crown

Crown, Heart, Spade

5 pence

5 pence on Crown

Heart, Anchor, Spade

0

3 pence on Crown, 2 on Spade

Crown, Crown, Crown

9 pence

3 pence on Crown, 2 on Spade

Crown, Spade, Anchor

5 pence

1 pence on all squares

Any roll

3 pence (not a good strategy!)

I chose this example because it is not too complex, and with a little imagination, demonstrates the main control flow statements. While it’s unlikely that you will ever need to simulate the gambling behaviors of 19th-century sailors, this type of simulation is quite common in many applications. In the case of Crown and Anchor, perhaps we have constructed a mathematical model to determine if we should open a Crown and Anchor booth to raise money for charity at our next company event. The simulation we construct in this chapter can be used to support the correctness of our model.

The game itself is simple, but there are many thousands of ways it could be played. Our sailor—let’s call him Thomas (a good, solid British name)—will start off very generic, and his behavior will become more detailed as we proceed.

Let’s begin with the basics: starting and stopping conditions. Every time Thomas gets shore leave, he takes 50 pence with him to spend on Crown and Anchor. Thomas has a limit: if he’s lucky enough to double his money, he quits, walking away with at least 100 pence in his pocket (about half his monthly wages). If he doesn’t double his money, he gambles until he’s broke.

We’ll break the playing of the game into three parts: placing the bets, rolling the dice, and collecting the winnings (if any). Now that we have a very simple, high-level picture of Thomas’s behavior, we can draw a flowchart to describe it, shown in Figure 4-1.

Crown and Anchor simulation flowchart
Figure 4-1. Crown and Anchor simulation flowchart

In a flowchart, the diamond shapes represent “yes or no” decisions, and the rectangles represent actions. We use circles to describe where to start and end.

The flowchart—as we’ve drawn it—isn’t quite ready to be turned directly into a program. The steps here are easy for a human to understand, but too sophisticated for computers. For example, “roll dice” would not be obvious to a computer. What are dice? How do you roll them? To solve this problem, the steps “place bets,” “roll dice,” and “collect winnings” will have their own flowcharts (we have indicated this in the flowchart by shading those actions). If you had a big enough piece of paper, you could put them all together, but for the purposes of this book, we’ll present them separately.

Also, our decision node is too vague for a computer: “broke or won 100 pence?” isn’t something a computer can understand. So what can a computer understand? For the purposes of this chapter, we’ll restrict our flowchart actions to the following:

  • Variable assignment: funds = 50, bets = {}, hand = []

  • Random integer between m and n, inclusive: rand(1, 6) (this is a “helper function” we will be providing later)

  • Random face string (“heart,” “crown,” etc.): randFace() (another helper function)

  • Object property assignment: bets["heart"] = 5, bets[randFace()] = 5

  • Adding elements to an array: hand.push(randFace())

  • Basic arithmetic: funds - totalBet, funds + winnings

  • Increment: roll++ (this is a common shorthand that just means “add one to the variable roll”)

And we’ll limit our flowchart decisions to the following:

  • Numeric comparisons (funds > 0, funds < 100)

  • Equality comparisons (totalBet === 7; we’ll learn why we use three equals signs in Chapter 5)

  • Logical operators (funds > 0 && funds < 100; the double ampersand means “and,” which we’ll learn about in Chapter 5)

All of these “allowed actions” are actions that we can write in JavaScript with little or no interpretation or translation.

One final vocabulary note: throughout this chapter, we will be using the words truthy and falsy. These are not simply diminutive or “cute” versions of true and false: they have meaning in JavaScript. What these terms mean will be explained in Chapter 5, but for now you can just translate them to “true” and “false” in your head.

Now that we know the limited language we can use, we’ll have to rewrite our flowchart as shown in Figure 4-2.

Crown and Anchor simulation flowchart (refined)
Figure 4-2. Crown and Anchor simulation flowchart (refined)

while Loops

We finally have something that we can translate directly into code. Our flowchart already has the first control flow statement we’ll be discussing, a while loop. A while loop repeats code as long as its condition is met. In our flowchart, the condition is funds > 1 && funds < 100. Let’s see how that looks in code:

let funds = 50;     // starting conditions

while(funds > 0 && funds < 100) {
    // place bets

    // roll dice

    // collect winnings
}

If we run this program as is, it will run forever, because funds start at 50 pence, and they never increase or decrease, so the condition is always true. Before we start filling out details, though, we need to talk about block statements.

Block Statements

Block statements (sometimes called compound statements) are not control flow statements, but they go hand in hand with them. A block statement is just a series of statements enclosed in curly braces that is treated by JavaScript as a single unit. While it is possible to have a block statement by itself, it has little utility. For example:

{   // start block statement
    console.log("statement 1");
    console.log("statement 2");
}   // end block statement

console.log("statement 3");

The first two calls to console.log are inside a block; this is a meaningless but valid example.

Where block statements become useful is with control flow statements. For example, the loop we’re executing with our while statement will execute the entire block statement before testing the condition again. For example, if we wanted to take “two steps forward and one step back,” we could write:

let funds = 50;     // starting conditions

while(funds > 0 && funds < 100) {

    funds = funds + 2;  // two steps forward

    funds = funds - 1;  // one step back
}

This while loop will eventually terminate: every time through the loop, funds increases by two and decreases by one for a net of 1. Eventually, funds will be 100 and the loop will terminate.

While it’s very common to use a block statement with control flow, it’s not required. For example, if we wanted to simply count up to 100 by twos, we don’t need a block statement:

let funds = 50;     // starting conditions

while(funds > 0 && funds < 100)
    funds = funds + 2;

Whitespace

For the most part, JavaScript doesn’t care about additional whitespace (including newlines2): 1 space is as good as 10, or 10 spaces and 10 newlines. This doesn’t mean you should use whitespace capriciously. For example, the preceding while statement is equivalent to:

while(funds > 0 && funds < 100)

funds = funds + 2;

But this hardly makes it look as if the two statements are connected! Using this formatting is very misleading, and should be avoided. The following equivalents, however, are relatively common, and mostly unambiguous:

// no newline
while(funds > 0 && funds < 100) funds = funds + 2;

// no newline, block with one statement
while(funds > 0 && funds < 100) { funds = funds + 2; }

There are those who insist that control flow statement bodies—for the sake of consistency and clarity—should always be a block statement (even if they contain only one statement). While I do not fall in this camp, I should point out that careless indentation fuels the fire of this argument:

while(funds > 0 && funds < 100)
    funds = funds + 2;
    funds = funds - 1;

At a glance, it looks like the body of the while loop is executing two statements (two steps forward and one step back), but because there’s no block statement here, JavaScript interprets this as:

while(funds > 0 && funds < 100)
    funds = funds + 2;              // while loop body

funds = funds - 1;                  // after while loop

I side with those who say that omitting the block statement for single-line bodies is acceptable, but of course you should always be responsible with indentation to make your meaning clear. Also, if you’re working on a team or an open source project, you should adhere to any style guides agreed upon by the team, regardless of your personal preferences.

While there is disagreement on the issue of using blocks for single-statement bodies, one syntactically valid choice is nearly universally reviled: mixing blocks and single statements in the same if statement:

// don't do this
if(funds > 0) {
    console.log("There's money left!");
    console.log("That means keep playing!");
} else
    console.log("I'm broke!  Time to quit.");

// or this
if(funds > 0)
    console.log("There's money left!  Keep playing!");
else {
    console.log("I'm broke"!);
    console.log("Time to quit.")
}

Helper Functions

To follow along with the examples in this chapter, we’ll need two helper functions. We haven’t learned about functions yet (or pseudorandom number generation), but we will in upcoming chapters. For now, copy these two helper functions verbatim:

// returns a random integer in the range [m, n] (inclusive)
function rand(m, n) {
    return m + Math.floor((n - m + 1)*Math.random());
}

// randomly returns a string representing one of the six
// Crown and Anchor faces
function randFace() {
    return ["crown", "anchor", "heart", "spade", "club", "diamond"]
        [rand(0, 5)];
}

if…else Statement

One of the shaded boxes in our flowchart is “place bets,” which we’ll fill out now. So how does Thomas place bets? Thomas has a ritual, as it turns out. He reaches into his right pocket and randomly pulls out a handful of coins (as few as one, or as many as all of them). That will be his funds for this round. Thomas is superstitious, however, and believes the number 7 is lucky. So if he happens to pull out 7 pence, he reaches back into his pocket and bets all his money on the “Heart” square. Otherwise, he randomly places the bet on some number of squares (which, again, we’ll save for later). Let’s look at the “place bets” flowchart in Figure 4-3.

Crown and Anchor simulation: 'place bets' flowchart
Figure 4-3. Crown and Anchor simulation: place bets flowchart

The decision node in the middle (totalBet === 7) here represents an if...else statement. Note that, unlike the while statement, it doesn’t loop back on itself: the decision is made, and then you move on. We translate this flowchart into JavaScript:

const bets = { crown: 0, anchor: 0, heart: 0,
    spade: 0, club: 0, diamond: 0 };
let totalBet = rand(1, funds);
if(totalBet === 7) {
    totalBet = funds;
    bets.heart = totalBet;
} else {
    // distribute total bet
}
funds = funds - totalBet;

We’ll see later that the else part of the if...else statement is optional.

do…while Loop

When Thomas doesn’t pull out 7 pence by chance, he randomly distributes the funds among the squares. He has a ritual for doing this: he holds the coins in his right hand, and with his left hand, selects a random number of them (as few as one, and as many as all of them), and places it on a random square (sometimes he places a bet on the same square more than once). We can now update our flowchart to show this random distribution of the total bet, as shown in Figure 4-4.

Crown and Anchor simulation: 'distribute bets' flowchart
Figure 4-4. Crown and Anchor simulation: distribute bets flowchart

Note how this differs from the while loop: the decision comes at the end, not the beginning. do...while loops are for when you know you always want to execute the body of the loop at least once (if the condition in a while loop starts off as falsy, it won’t even run once). Now in JavaScript:

let remaining = totalBet;
do {
    let bet = rand(1, remaining);
    let face = randFace();
    bets[face] = bets[face] + bet;
    remaining = remaining - bet;
} while(remaining > 0);

for Loop

Thomas has now placed all his bets! Time to roll the dice.

The for loop is extremely flexible (it can even replace a while or do...while loop), but it’s best suited for those times when you need to do things a fixed number of times (especially when you need to know which step you’re on), which makes it ideal for rolling a fixed number of dice (three, in this case). Let’s start with our “roll dice” flowchart, shown in Figure 4-5.

Crown and Anchor simulation: 'roll dice' flowchart
Figure 4-5. Crown and Anchor simulation: roll dice flowchart

A for loop consists of three parts: the initializer (roll = 0), the condition (roll < 3), and the final expression (roll++). It’s nothing that can’t be constructed with a while loop, but it conveniently puts all the loop information in one place. Here’s how it looks in JavaScript:

const hand = [];
for(let roll = 0; roll < 3; roll++) {
    hand.push(randFace());
}

Programmers have a tendency to count from 0, which is why we start at roll 0 and stop at roll 2.

Tip

It has become a convention to use the variable i (shorthand for “index”) in a for loop, no matter what you’re counting, though you can use whatever variable name you wish. I chose roll here to be clear that we’re counting the number of rolls, but I had to catch myself: when I first wrote out this example, I used i out of habit!

if Statement

We’re almost there! We’ve placed our bets and rolled our hand; all that’s left is to collect any winnings. We have three random faces in the hand array, so we’ll use another for loop to see if any of them are winners. To do that, we’ll use an if statement (this time without an else clause). Our final flowchart is shown in Figure 4-6.

Notice the difference between an if...else statement and an if statement: only one of the if statement’s branches lead to an action, whereas both of the if...else statement’s do. We translate this into code for the final piece of the puzzle:

let winnings = 0;
for(let die=0; die < hand.length; die++) {
    let face = hand[die];
    if(bets[face] > 0) winnings = winnings + bets[face];
}
funds = funds + winnings;

Note that, instead of counting to 3 in the for loop, we count to hand.length (which happens to be 3). The goal of this part of the program is to calculate the winnings for any hand. While the rules of the game call for a hand of three dice, the rules could change…or perhaps more dice are given as a bonus, or fewer dice are given as a penalty. The point is, it costs us very little to make this code more generic. If we change the rules to allow more or fewer dice in a hand, we don’t have to worry about changing this code: it will do the correct thing no matter how many dice there are in the hand.

Crown and Anchor simulation: 'collect winnings' flowchart
Figure 4-6. Crown and Anchor simulation: collect winnings flowchart

Putting It All Together

We would need a large piece of paper to put all of the pieces of the flowchart together, but we can write the whole program out fairly easily.

In the following program listing (which includes the helper functions), there are also some calls to console.log so you can observe Thomas’s progress (don’t worry about understanding how the logging works: it’s using some advanced techniques we’ll learn about in later chapters).

We also add a round variable to count the number of rounds Thomas plays, for display purposes only:

// returns a random integer in the range [m, n] (inclusive)
function rand(m, n) {
    return m + Math.floor((n - m + 1)*Math.random());
}

// randomly returns a string representing one of the six
// Crown and Anchor faces
function randFace() {
    return ["crown", "anchor", "heart", "spade", "club", "diamond"]
        [rand(0, 5)];
}

let funds = 50;     // starting conditions
let round = 0;

while(funds > 0 && funds < 100) {
    round++;
    console.log(`round ${round}:`);
    console.log(`\tstarting funds: ${funds}p`);
    // place bets
    let bets = { crown: 0, anchor: 0, heart: 0,
        spade: 0, club: 0, diamond: 0 };
    let totalBet = rand(1, funds);
    if(totalBet === 7) {
        totalBet = funds;
        bets.heart = totalBet;
    } else {
        // distribute total bet
        let remaining = totalBet;
        do {
            let bet = rand(1, remaining);
            let face = randFace();
            bets[face] = bets[face] + bet;
            remaining = remaining - bet;
        } while(remaining > 0)
    }
    funds = funds - totalBet;
    console.log('\tbets: ' +
        Object.keys(bets).map(face => `${face}: ${bets[face]} pence`).join(', ') +
        ` (total: ${totalBet} pence)`);

    // roll dice
    const hand = [];
    for(let roll = 0; roll < 3; roll++) {
        hand.push(randFace());
    }
    console.log(`\thand: ${hand.join(', ')}`);

    // collect winnings
    let winnings = 0;
    for(let die=0; die < hand.length; die++) {
        let face = hand[die];
        if(bets[face] > 0) winnings = winnings + bets[face];
    }
    funds = funds + winnings;
    console.log(`\twinnings: ${winnings}`);
}
console.log(`\tending funds: ${funds}`);

Control Flow Statements in JavaScript

Now that we’ve got a firm grasp on what control flow statements actually do, and some exposure to the most basic ones, we can get down to the details of JavaScript control flow statements.

We’re also going to leave flowcharts behind. They are a great visualization tool (especially for those who are visual learners), but they would get very unwieldy past this point.

Broadly speaking, control flow can be broken into two subcategories: conditional (or branching) control flow and loop control flow. Conditional control flow (if and if...else, which we’ve seen, and switch, which we’ll see shortly) represent a fork in the road: there are two or more paths to take, and we take one, but we don’t double back. Loop control flow (while, do...while, and for loops) repeat their bodies until a condition is met.

Control Flow Exceptions

There are four statements that can alter the normal processing of flow control. You can think of these as control flow “trump cards”:

break

Breaks out of loop early.

continue

Skip to the next step in the loop.

return

Exits the current function (regardless of control flow). See Chapter 6.

throw

Indicates an exception that must be caught by an exception handler (even if it’s outside of the current control flow statement). See Chapter 11.

The use of the statements will become clear as we go along; the important thing to understand now is that these four statements can override the behavior of the control flow constructs we’ll be discussing.

Broadly speaking, control flow can be broken into two subcategories: conditional control flow and loop control flow.

Chaining if...else Statements

Chaining if...else statements is not actually a special syntax: it’s simply a series of if...else statements where each else clause contains another if...else. It’s a common enough pattern that it deserves mention. For example, if Thomas’s superstition extends to days of the week, and he’ll only bet a single penny on a Wednesday, we could combine this logic in an if...else chain:

if(new Date().getDay() === 3) {   // new Date().getDay() returns the current
    totalBet = 1;                 // numeric day of the week, with 0 = Sunday
} else if(funds === 7) {
    totalBet = funds;
} else {
    console.log("No superstition here!");
}

By combining if...else statements this way, we’ve created a three-way choice, instead of simply a two-way choice. The astute reader may note that we’re technically breaking the guideline we established (of not mixing single statements and block statements), but this is an exception to the rule: it is a common pattern, and is not confusing to read. We could rewrite this using block statements:

if(new Date().getDay() === 3) {
    totalBet = 1;
} else {
    if(funds === 7) {
        totalBet = funds;
    } else {
        console.log("No superstition here!");
    }
}

We haven’t really gained any clarity, and we’ve made our code more verbose.

Metasyntax

The term metasyntax means a syntax that, in turn, describes or communicates yet another syntax. Those with a computer science background will immediately think of “Extended Backus-Naur Form” (EBNF), which is an incredibly intimidating name for a simple concept.

For the rest of this chapter, I’ll be using a metasyntax to concisely describe JavaScript flow control syntax. The metasyntax I’m using is simple and informal and—most importantly—used for JavaScript documentation on the Mozilla Developer Network (MDN). Because the MDN is a resource you will undoubtedly find yourself using quite often, familiarity with it will be useful.

There are only two real elements to this metasyntax: something surrounded by square brackets is optional, and an ellipsis (three periods, technically) indicates “more goes here.” Words are used as placeholders, and their meaning is clear from context. For example, statement1 and statement2 represent two different statements, expression is something that results in a value, and condition refers to an expression that is treated as truthy or falsy.

Tip

Remember that a block statement is a statement…so wherever you can use a statement, you can use a block statement.

Because we’re already familiar with some control flow statements, let’s see their metasyntax:

while statement

while(condition)
    statement

While condition is truthy, statement will be executed.

if...else statement

if(condition)
    statement1
[else
    statement2]

If condition is truthy, statement1 will be executed; otherwise, statement2 will be executed (assuming the else part is present).

do...while statement

do
    statement
while(condition);

statement is executed at least once, and is repeatedly executed as long as condition is truthy.

for statement

for([initialization]; [condition]; [final-expression])
    statement

Before the loop runs, initialization is executed. As long as condition is true, statement is executed, then final-expression is executed before testing condition again.

Additional for Loop Patterns

By using the comma operator (which we’ll learn more about in Chapter 5), we can combine multiple assignments and final expressions. For example, here’s a for loop to print the first eight Fibonacci numbers:

for(let temp, i=0, j=1; j<30; temp = i, i = j, j = i + temp)
    console.log(j);

In this example, we’re declaring multiple variables (temp, i, and j), and we’re modifying each of them in the final expression. Just as we can do more with a for loop by using the comma operator, we can use nothing at all to create an infinite loop:

for(;;) console.log("I will repeat forever!");

In this for loop, the condition is omitted, which JavaScript interprets as true, meaning the loop will never have cause to exit.

While the most common use of for loops is to increment or decrement integer indices, that is not a requirement: any expression will work. Here are some examples:

let s = '3';                        // string containing a number
for(; s.length<10; s = ' ' + s);    // zero pad string; note that we must
                                    // include a semicolon to terminate
                                    // this for loop!

for(let x=0.2; x<3.0; x += 0.2)     // increment using noninteger
    console.log(x);

for(; !player.isBroke;)             // use an object property as conditional
    console.log("Still playing!");

Note that a for loop can always be written as a while loop. In other words:

for([initialization]; [condition]; [final-expression])
    statement

is equivalent to:

[initialization]
while([condition]) {
    statement
    [final-expression]
}

However, the fact that you can write a for loop as a while loop doesn’t mean you should. The advantage of the for loop is that all of the loop control information is right there on the first line, making it very clear what’s happening. Also, with a for loop, initializing variables with let confines them to the body of the for loop (we’ll learn more about this in Chapter 7); if you convert such a for statement to a while statement, the control variable(s) will, by necessity, be available outside of the for loop body.

switch Statements

Where if...else statements allow you to take one of two paths, switch statements allow you to take multiple paths based on a single condition. It follows, then, that the condition must be something more varied than a truthy/falsy value: for a switch statement, the condition is an expression that evaluates to a value. The syntax of a switch statement is:

switch(expression) {
    case value1:
        // executed when the result of expression matches value1
        [break;]
    case value2:
        // executed when the result of expression matches value2
        [break;]
        ...
    case valueN:
        // executed when the result of expression matches valueN
        [break;]
    default:
        // executed when none of the values match the value of expression
        [break;]
}

JavaScript will evaluate expression, pick the first case that matches, and then execute statements until it sees a break, return, continue, throw or the end of the switch statement (we will learn about return, continue, and throw later). If that sounds complex to you, you’re not alone: because of the nuances of the switch statement, it’s received a lot of criticism as being a common source of programmer error. Often, beginning programmers are discouraged from using it at all. I feel that the switch statement is very useful in the right situation: it’s a good tool to have in your toolbox, but like any tool, you should exercise caution, and use it when appropriate.

We’ll start with a very straightforward example of a switch statement. If our fictional sailor has multiple numbers he’s superstitious about, we can use a switch statement to handle them accordingly:

switch(totalBet) {
    case 7:
        totalBet = funds;
        break;
    case 11:
        totalBet = 0;
        break;
    case 13:
        totalBet = 0;
        break;
    case 21:
        totalBet = 21;
        break;
}

Note that the same action is being taken when the bet is 11 or 13. This is where we might want to take advantage of fall-through execution. Remember that we said the switch statement will keep executing statements until it sees a break statement. Using this to our advantage is called fall-through execution:

switch(totalBet) {
    case 7:
        totalBet = funds;
        break;
    case 11:
    case 13:
        totalBet = 0;
        break;
    case 21:
        totalBet = 21;
        break;
}

This is pretty straightforward so far: it’s clear that Thomas won’t bet anything if he happens to pull out 11 or 13 pence. But what if 13 is a far more ominous omen than 11, and requires not only forgoing the bet, but giving a penny to charity? With some clever rearranging, we can handle this:

switch(totalBet) {
    case 7:
        totalBet = funds;
        break;
    case 13:
        funds = funds - 1;  // give 1 pence to charity!
    case 11:
        totalBet = 0;
        break;
    case 21:
        totalBet = 21;
        break;
}

If totalBet is 13, we give a penny to charity, but because there’s no break statement, we fall through to the next case (11), and additionally set totalBet to 0. This code is valid JavaScript, and furthermore, it is correct: it does what we intended it to do. However, it does have a weakness: it looks like a mistake (even though it’s correct). Imagine if a colleague saw this code and thought “Ah, there’s supposed to be a break statement there.” They would add the break statement, and the code would no longer be correct. Many people feel that fall-through execution is more trouble than it’s worth, but if you choose to utilize this feature, I recommend always including a comment to make it clear that your use of it is intentional.

You can also specify a special case, called default, that will be used if no other case matches. It is conventional (but not required) to put the default case last:

switch(totalBet) {
    case 7:
        totalBet = funds;
        break;
    case 13:
        funds = funds - 1;  // give 1 pence to charity!
    case 11:
        totalBet = 0;
        break;
    case 21:
        totalBet = 21;
        break;
    default:
        console.log("No superstition here!");
        break;
}

The break is unnecessary because no cases follow default, but always providing a break statement is a good habit to get into. Even if you’re using fall-through execution, you should get into the habit of including break statements: you can always replace a break statement with a comment to enable fall-through execution, but omitting a break statement when it is correct can be a very difficult-to-locate defect. The one exception to this rule of thumb is if you are using a switch statement inside a function (see Chapter 6), you can replace break statements with return statements (because they immediately exit the function):

function adjustBet(totalBet, funds) {
    switch(totalBet) {
        case 7:
            return funds;
        case 13:
            return 0;
        default:
            return totalBet;
    }
}

As usual, JavaScript doesn’t care how much whitespace you use, so it’s quite common to put the break (or return) on the same line to make switch statements more compact:

switch(totalBet) {
    case 7: totalBet = funds; break;
    case 11: totalBet = 0; break;
    case 13: totalBet = 0; break;
    case 21: totalBet = 21; break;
}

Note that, in this example, we chose to repeat the same action for 11 and 13: omitting the newline is clearest when cases have single statements and no fall-through execution.

switch statements are extremely handy when you want to take many different paths based on a single expression. That said, you will find yourself using them less once you learn about dynamic dispatch in Chapter 9.

for...in loop

The for...in loop is designed to loop over the property keys of an object. The syntax is:

for(variable in object)
    statement

Here is an example of its use:

const player = { name: 'Thomas', rank: 'Midshipman', age: 25 };
for(let prop in player) {
    if(!player.hasOwnProperty(prop)) continue;  // see explanation below
    console.log(prop + ': ' + player[prop]);
}

Don’t worry if this seems confusing now; we’ll learn more about this example in Chapter 9. In particular, the call to player.hasOwnProperty is not required, but its omission is a common source of errors, which will be covered in Chapter 9. For now, all you need to understand is that this is a type of looping control flow statement.

for...of loop

New in ES6, the for...of operator provides yet another way to loop over the elements in a collection. Its syntax is:

for(variable of object)
    statement

The for...of loop can be used on arrays, but more generically, on any object that is iterable (see Chapter 9). Here is an example of its use for looping over the contents of an array:

const hand = [randFace(), randFace(), randFace()];
for(let face of hand)
    console.log(`You rolled...${face}!`);

for...of is a great choice when you need to loop over an array, but don’t need to know the index number of each element. If you need to know the indexes, use a regular for loop:

const hand = [randFace(), randFace(), randFace()];
for(let i=0; i<hand.length; i++)
    console.log(`Roll ${i+1}: ${hand[i]}`);

Useful Control Flow Patterns

Now that you know the basics of control flow constructs in JavaScript, we turn our attention to some of the common patterns that you’ll encounter.

Using continue to Reduce Conditional Nesting

Very often, in the body of a loop, you’ll only want to continue to execute the body under certain circumstances (essentially combining a loop control flow with a conditional control flow). For example:

while(funds > 0 && funds < 100) {
    let totalBet = rand(1, funds);
    if(totalBet === 13) {
        console.log("Unlucky!  Skip this round....");
    } else {
        // play...
    }
}

This is an example of nested control flow; inside the body of the while loop, the bulk of the action is inside the else clause; all we do inside the if clause is to call console.log. We can use continue statements to “flatten” this structure:

while(funds > 0 && funds < 100) {
    let totalBet = rand(1, funds);
    if(totalBet === 13) {
        console.log("Unlucky!  Skip this round....");
        continue;
    }
    // play...
}

In this simple example, the benefits aren’t immediately obvious, but imagine that the loop body consisted of not 1 line, but 20; by removing those lines from the nested control flow, we’re making the code easier to read and understand.

Using break or return to Avoid Unnecessary Computation

If your loop exists solely to find something and then stop, there’s no point in executing every step if you find what you’re looking for early.

For example, determining if a number is prime is relatively expensive, computationally speaking. If you’re looking for the first prime in a list of thousands of numbers, a naive approach might be:

let firstPrime = null;
for(let n of bigArrayOfNumbers) {
    if(isPrime(n) && firstPrime === null) firstPrime = n;
}

If bigArrayOfNumbers has a million numbers, and only the last one is prime (unbeknownst to you), this approach would be fine. But what if the first one were prime? Or the fifth, or the fiftieth? You would be checking to see if a million numbers are prime when you could have stopped early! Sounds exhausting. We can use a break statement stop as soon as we’ve found what we’re looking for:

let firstPrime = null;
for(let n of bigArrayOfNumbers) {
    if(isPrime(n)) {
        firstPrime = n;
        break;
    }
}

If this loop is inside a function, we could use a return statement instead of break.

Using Value of Index After Loop Completion

Occasionally, the important output of a loop is the value of the index variable when the loop terminates early with a break. We can take advantage of the fact that when a for loop finishes, the index variable retains its value. If you employ this pattern, keep in mind the edge case where the loop completes successfully without a break. For example, we can use this pattern to find the index of the first prime number in our array:

let i = 0;
for(; i < bigArrayOfNumbers.length; i++) {
    if(isPrime(bigArrayOfNumbers[i])) break;
}
if(i === bigArrayOfNumbers.length) console.log('No prime numbers!');
else console.log(`First prime number found at position ${i}`);

Using Descending Indexes When Modifying Lists

Modifying a list while you’re looping over the elements of the list can be tricky, because by modifying the list, you could be modifying the loop termination conditions. At best, the output won’t be what you expect; at worst, you could end up with an infinite loop. One common way to deal with this is to use descending indexes to start at the end of the loop and work your way toward the beginning. In this way, if you add or remove elements from the list, it won’t affect the termination conditions of the loop.

For example, we might want to remove all prime numbers from bigArrayOfNumbers. We’ll use an array method called splice, which can add or remove elements from an array (see Chapter 8). The following will not work as expected:

for(let i=0; i<bigArrayOfNumbers.length; i++) {
    if(isPrime(bigArrayOfNumbers[i])) bigArrayOfNumbers.splice(i, 1);
}

Because the index is increasing, and we’re removing elements, there’s a possibility that we might skip over primes (if they are adjacent). We can solve this problem by using descending indexes:

for(let i=bigArrayOfNumbers.length-1; i >= 0; i--) {
    if(isPrime(bigArrayOfNumbers[i])) bigArrayOfNumbers.splice(i, 1);
}

Note carefully the initial and test conditions: we have to start with one less than the length of the array, because arrays indexes are zero-based. Also, we continue the loop as long as i is greater than or equal to 0; otherwise, this loop won’t cover the first element in the array (which would cause problems if the first element were a prime).

Conclusion

Control flow is really what makes our programs tick. Variables and constants may contain all the interesting information, but control flow statements allow us to make useful choices based on that data.

Flowcharts are a useful way to describe control flow visually, and very often it can be helpful to describe your problem with a high-level flowchart before you start writing code. At the end of the day, however, flowcharts are not very compact, and code is a more efficient and (with practice) natural way to express control flow (many attempts have been made to construct programming languages that were visual only, yet text-based languages have never been threatened by this usurper).

1 As loath as I am to use a gender-specific example, women didn’t serve in the Royal Navy until 1917.

2 Newlines after return statements will cause problems; see Chapter 6 for more information.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required