Chapter 13. Statements

This chapter covers JavaScript’s statements: variable declarations, loops, conditionals, and others.

Declaring and Assigning Variables

var is used to declare a variable, which creates the variable and enables you to work with it. The equals operator (=) is used to assign a value to it:

var foo;
foo = 'abc';

var also lets you combine the preceding two statements into a single one:

var foo = 'abc';

Finally, you can also combine multiple var statements into one:

var x, y=123, z;

Read more about how variables work in Chapter 16.

The Bodies of Loops and Conditionals

Compound statements such as loops and conditionals have one or more “bodies” embedded—for example, the while loop:

while («condition»)
    «statement»

For the body «statement», you have a choice. You can either use a single statement:

while (x >= 0) x--;

or you can use a block (which counts as a single statement):

while (x > 0) {
    x--;
}

You need to use a block if you want the body to comprise multiple statements. Unless the complete compound statement can be written in a single line, I recommend using a block.

Loops

This section explores JavaScript’s loop statements.

Mechanisms to Be Used with Loops

The following mechanisms can be used with all loops:

break ⟦«label»⟧
Exit from a loop.
continue ⟦«label»⟧
Stop the current loop iteration, and immediately continue with the next one.
Labels

A label is an identifier followed by a colon. In front of a loop, a label allows you to break or continue that loop even from a loop nested inside of it. In front of a block, you can break out of that block. In both cases, the name of the label becomes an argument of break or continue. Here’s an example of breaking out of a block:

function findEvenNumber(arr) {
    loop: { // label
        for (var i=0; i<arr.length; i++) {
            var elem = arr[i];
            if ((elem % 2) === 0) {
                console.log('Found: ' + elem);
                break loop;
            }
        }
        console.log('No even number found.');
    }
    console.log('DONE');
}

while

A while loop:

while («condition»)
    «statement»

executes statement as long as condition holds. If condition is always true, you get an infinite loop:

while (true) { ... }

In the following example, we remove all elements of an array and log them to the console:

var arr = [ 'a', 'b', 'c' ];
while (arr.length > 0) {
    console.log(arr.shift());
}

Here is the output:

a
b
c

do-while

A do-while loop:

do «statement»
while («condition»);

executes statement at least once and then as long as condition holds. For example:

var line;
do {
    line = prompt('Enter a number:');
} while (!/^[0-9]+$/.test(line));

for

In a for loop:

for (⟦«init»⟧; ⟦«condition»⟧; ⟦«post_iteration»⟧)
    «statement»

init is executed once before the loop, which continues as long as condition is true. You can use var in init to declare variables, but the scope of those variables is always the complete surrounding function. post_iteration is executed after each iteration of the loop. Taking all of this into consideration, the preceding loop is equivalent to the following while loop:

«init»;
while («condition») {
    «statement»
    «post_iteration»;
}

The following example is the traditional way of iterating over arrays (other possibilities are described in Best Practices: Iterating over Arrays):

var arr = [ 'a', 'b', 'c' ];
for (var i=0; i<arr.length; i++) {
    console.log(arr[i]);
}

A for loop becomes infinite if you omit all parts of the head:

for (;;) {
    ...
}

for-in

A for-in loop:

for («variable» in «object»)
    «statement»

iterates over all property keys of object, including inherited ones. However, properties that are marked as not enumerable are ignored (see Property Attributes and Property Descriptors). The following rules apply to for-in loops:

  • You can use var to declare variables, but the scope of those variables is always the complete surrounding function.
  • Properties can be deleted during iteration.

Best practice: don’t use for-in for arrays

Don’t use for-in to iterate over arrays. First, it iterates over indices, not over values:

> var arr = [ 'a', 'b', 'c' ];
> for (var key in arr) { console.log(key); }
0
1
2

Second, it also iterates over all (nonindex) property keys. The following example illustrates what happens when you add a property foo to an array:

> var arr = [ 'a', 'b', 'c' ];
> arr.foo = true;
> for (var key in arr) { console.log(key); }
0
1
2
foo

Thus, you are better off with a normal for loop or the array method forEach() (see Best Practices: Iterating over Arrays).

Best practice: be careful with for-in for objects

The for-in loop iterates over all (enumerable) properties, including inherited ones. That may not be what you want. Let’s use the following constructor to illustrate the problem:

function Person(name) {
    this.name = name;
}
Person.prototype.describe = function () {
    return 'Name: '+this.name;
};

Instances of Person inherit the property describe from Person.prototype, which is seen by for-in:

var person = new Person('Jane');
for (var key in person) {
    console.log(key);
}

Here is the output:

name
describe

Normally, the best way to use for-in is to skip inherited properties via hasOwnProperty():

for (var key in person) {
    if (person.hasOwnProperty(key)) {
        console.log(key);
    }
}

And here is the output:

name

There is one last caveat: person may have a property hasOwnProperty, which would prevent the check from working. To be safe, you have to refer to the generic method (see Generic Methods: Borrowing Methods from Prototypes) Object.prototype.hasOwnProperty directly:

for (var key in person) {
    if (Object.prototype.hasOwnProperty.call(person, key)) {
        console.log(key);
    }
}

There are other, more comfortable, means for iterating over property keys, which are described in Best Practices: Iterating over Own Properties.

for each-in

This loop exists only on Firefox. Don’t use it.

Conditionals

This section covers JavaScript’s conditional statements.

if-then-else

In an if-then-else statement:

if («condition»)
    «then_branch»
⟦else
    «else_branch»⟧

then_branch and else_branch can be either single statements or blocks of statements (see The Bodies of Loops and Conditionals).

Chaining if statements

You can chain several if statements:

if (s1 > s2) {
    return 1;
} else if (s1 < s2) {
    return -1;
} else {
    return 0;
}

Note that in the preceding example, all the else branches are single statements (if statements). Programming languages that only allow blocks for else branches need some kind of else-if branch for chaining.

Pitfall: dangling else

The else branch of the following example is called dangling, because it is not clear to which of the two if statements it belongs:

if («cond1») if («cond2») «stmt1» else «stmt2»

Here’s a simple rule: use braces. The preceding snippet is equivalent to the following code (where it is obvious who the else belongs to):

if («cond1») {
    if («cond2») {
        «stmt1»
    } else {
        «stmt2»
    }
}

switch

A switch statement:

switch («expression») {
    case «label1_1»:
    case «label1_2»:
        ...
        «statements1»
        ⟦break;⟧
    case «label2_1»:
    case «label2_2»:
        ...
        «statements2»
        ⟦break;⟧
    ...
    ⟦default:
        «statements_default»
        ⟦break;⟧⟧
}

evaluates expression and then jumps to the case clause whose label matches the result. If no label matches, switch jumps to the default clause if it exists or does nothing otherwise.

The “operand” after case can be any expression; it is compared via === with the parameter of switch.

If you don’t finish a clause with a terminating statement, execution continues into the next clause. The most frequently used terminating statement is break. But return and throw also work, even though they normally leave more than just the switch statement.

The following example illustrates that you don’t need to break if you use throw or return:

function divide(dividend, divisor) {
    switch (divisor) {
        case 0:
            throw 'Division by zero';
        default:
            return dividend / divisor;
    }
}

In this example, there is no default clause. Therefore, nothing happens if fruit matches none of the case labels:

function useFruit(fruit) {
    switch (fruit) {
        case 'apple':
            makeCider();
            break;
        case 'grape':
            makeWine();
            break;
        // neither apple nor grape: do nothing
    }
}

Here, there are multiple case labels in a row:

function categorizeColor(color) {
    var result;
    switch (color) {
        case 'red':
        case 'yellow':
        case 'blue':
            result = 'Primary color: '+color;
            break;
        case 'orange':
        case 'green':
        case 'violet':
            result = 'Secondary color: '+color;
            break;
        case 'black':
        case 'white':
            result = 'Not a color';
            break;
        default:
            throw 'Illegal argument: '+color;
    }
    console.log(result);
}

This example demonstrates that the value after case can be an arbitrary expression:

function compare(x, y) {
    switch (true) {
        case x < y:
            return -1;
        case x === y:
            return 0;
        default:
            return 1;
    }
}

The preceding switch statement looks for a match for its parameter true by going through the case clauses. If one of the case expressions evaluates to true, the corresponding case body is executed. Therefore, the preceding code is equivalent to the following if statement:

function compare(x, y) {
    if (x < y) {
        return -1;
    } else if (x === y) {
        return 0;
    } else {
        return 1;
    }
}

You normally should prefer the latter solution; it is more self-explanatory.

The with Statement

This section explains how the with statement works in JavaScript and why its use is discouraged.

Syntax and Semantics

The syntax of the with statement is as follows:

with («object»)
    «statement»

It turns the properties of object into local variables for statement. For example:

var obj = { first: 'John' };
with (obj) {
    console.log('Hello '+first); // Hello John
}

Its intended use is to avoid redundancy when accessing an object several times. The following is an example of code with redundancies:

foo.bar.baz.bla   = 123;
foo.bar.baz.yadda = 'abc';

with makes this shorter:

with (foo.bar.baz) {
    bla   = 123;
    yadda = 'abc';
}

The with Statement Is Deprecated

The use of the with statement is generally discouraged (the next section explains why). For example, it is forbidden in strict mode:

> function foo() { 'use strict'; with ({}); }
SyntaxError: strict mode code may not contain 'with' statements

Techniques for avoiding the with statement

Avoid code like this:

// Don't do this:
with (foo.bar.baz) {
    console.log('Hello '+first+' '+last);
}

Instead, use a temporary variable with a short name:

var b = foo.bar.baz;
console.log('Hello '+b.first+' '+b.last);

If you don’t want to expose the temporary variable b to the current scope, you can use an IIFE (see Introducing a New Scope via an IIFE):

(function () {
    var b = foo.bar.baz;
    console.log('Hello '+b.first+' '+b.last);
}());

You also have the option of making the object that you want to access a parameter of the IIFE:

(function (b) {
    console.log('Hello '+b.first+' '+b.last);
}(foo.bar.baz));

The Rationale for the Deprecation

To understand why with is deprecated, look at the following example and notice how the function’s argument completely changes how it works:

function logMessage(msg, opts) {
    with (opts) {
        console.log('msg: '+msg); // (1)
    }
}

If opts has a property msg, then the statement in line (1) doesn’t access the parameter msg anymore. It accesses the property:

> logMessage('hello', {})  // parameter msg
msg: hello
> logMessage('hello', { msg: 'world' })  // property opts.msg
msg: world

There are three problems that the with statement causes:

Performance suffers
Variable lookup becomes slower, because an object is temporarily inserted into the scope chain.
Code becomes less predictable

You cannot determine what an identifier refers to by looking at its syntactic surroundings (its lexical context). According to Brendan Eich, that was the actual reason why with was deprecated, not performance considerations:

with violates lexical scope, making program analysis (e.g. for security) hard to infeasible.

Minifiers (described in Chapter 32) can’t shorten variable names
Inside a with statement, you can’t statically determine whether a name refers to a variable or a property. Only variables can be renamed by minifiers.

Here is an example of with making code brittle:

function foo(someArray) {
    var values = ...;  // (1)
    with (someArray) {
        values.someMethod(...);  // (2)
        ...
    }
}
foo(myData);  // (3)

You can prevent the function call in line (3) from working, even if you don’t have access to the array myData.

How? By adding a property values to Array.prototype. For example:

Array.prototype.values = function () {
    ...
};

Now the code in line (2) calls someArray.values.someMethod() instead of values.someMethod(). The reason is that, inside the with statement, values now refers to someArray.values and not the local variable from line (1) anymore.

This is not just a thought experiment: the array method values() was added to Firefox and broke the TYPO3 content management system. Brandon Benvie figured out what went wrong.

The debugger Statement

The syntax for the debugger statement is as follows:

debugger;

If a debugger is active, this statement functions as a breakpoint; if not, it has no observable effect.

Get Speaking JavaScript now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.