第13章 ステートメント
目次
書籍を購入する
(広告です。ブロックしないでください。)

第13章 ステートメント

この章では、JavaScriptのステートメントについて説明します。変数宣言、ループ、条件分岐などです。

変数の宣言と代入

varは変数を宣言するために使用されます。変数を作成し、操作できるようにします。等号演算子(=)は、変数に値を代入するために使用されます。

var foo;
foo = 'abc';

varを使用すると、上記の2つのステートメントを1つにまとめることもできます。

var foo = 'abc';

さらに、複数のvarステートメントを1つにまとめることもできます。

var x, y=123, z;

変数の仕組みの詳細については、第16章を参照してください。

ループと条件分岐の本体

複合ステートメント(ループや条件分岐など)には、1つ以上の「本体」が埋め込まれています。たとえば、whileループは次のようになります。

while («condition»)
    «statement»

本体の«statement»には、選択肢があります。単一のステートメントを使用することもできます。

while (x >= 0) x--;

または、ブロック(単一のステートメントとしてカウントされます)を使用することもできます。

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

本体を複数のステートメントで構成する場合は、ブロックを使用する必要があります。複合ステートメント全体を1行で記述できる場合を除き、ブロックを使用することをお勧めします。

ループ

このセクションでは、JavaScriptのループステートメントについて説明します。

ループで使用されるメカニズム

以下のメカニズムは、すべてのループで使用できます。

break ⟦«label»⟧
ループから抜け出します。
continue ⟦«label»⟧
現在のループの反復を停止し、すぐに次の反復に進みます。
ラベル

ラベルは、識別子の後にコロンを付けたものです。ループの前にラベルを付けると、ネストされたループ内からでも、そのループをbreakまたはcontinueできます。ブロックの前にラベルを付けると、そのブロックから抜け出すことができます。どちらの場合も、ラベルの名前はbreakまたはcontinueの引数になります。ブロックから抜け出す例を次に示します。

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

whileループ:

while («condition»)
    «statement»

conditionが成立する限り、statementを実行します。conditionが常にtrueの場合、無限ループになります。

while (true) { ... }

次の例では、配列のすべての要素を削除し、コンソールに記録します。

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

出力は次のとおりです。

a
b
c

do-while

do-whileループ

do «statement»
while («condition»);

statementを少なくとも1回実行し、その後、conditionが成立する限り実行します。次に例を示します。

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

for

forループ

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

initはループの前に1回実行され、conditiontrueである限りループは続きます。initvarを使用して変数を宣言できますが、それらの変数のスコープは常に周囲の関数全体です。post_iterationは、ループの各反復の後に実行されます。これらをすべて考慮すると、上記のループは次のwhileループと同等です。

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

次の例は、配列を反復処理する従来の方法です(他の可能性については、ベストプラクティス:配列の反復処理を参照してください)。

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

ヘッドのすべての部分を省略すると、forループは無限になります。

for (;;) {
    ...
}

for-in

for-inループ

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

継承されたものを含め、objectのすべてのプロパティキーを反復処理します。ただし、列挙不可としてマークされているプロパティは無視されます(プロパティ属性とプロパティ記述子を参照)。for-inループには、次の規則が適用されます。

  • varを使用して変数を宣言できますが、それらの変数のスコープは常に周囲の関数全体です。
  • 反復処理中にプロパティを削除できます。

ベストプラクティス:配列にはfor-inを使用しない

配列を反復処理するためにfor-inを使用しないでください。まず、値ではなくインデックスを反復処理します。

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

次に、すべての(非インデックス)プロパティキーも反復処理します。次の例は、配列にプロパティfooを追加したときに何が起こるかを示しています。

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

したがって、通常のforループまたは配列メソッドforEach()の方が適しています(ベストプラクティス:配列の反復処理を参照)。

ベストプラクティス:オブジェクトにはfor-inに注意する

for-inループは、継承されたものを含め、すべての(列挙可能な)プロパティを反復処理します。これは、あなたが望むものではないかもしれません。次のコンストラクタを使用して、問題を説明しましょう。

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

Personのインスタンスは、Person.prototypeからプロパティdescribeを継承します。これは、for-inによって認識されます。

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

出力は次のとおりです。

name
describe

通常、for-inを使用する最良の方法は、hasOwnProperty()を使用して継承されたプロパティをスキップすることです。

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

出力は次のとおりです。

name

最後の注意点が1つあります。personにはプロパティhasOwnPropertyがある場合があり、チェックが機能しなくなる可能性があります。安全のため、汎用メソッド(汎用メソッド:プロトタイプからのメソッドの借用を参照)Object.prototype.hasOwnPropertyを直接参照する必要があります。

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

プロパティキーを反復処理するための、より快適な方法が他にもあります。これらについては、ベストプラクティス:自身のプロパティの反復処理で説明しています。

for each-in

このループはFirefoxにのみ存在します。使用しないでください。

条件分岐

このセクションでは、JavaScriptの条件分岐ステートメントについて説明します。

if-then-else

if-then-elseステートメント

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

then_branchelse_branchは、単一のステートメントまたはステートメントのブロックにすることができます(ループと条件分岐の本体を参照)。

ifステートメントの連鎖

複数のifステートメントを連鎖させることができます。

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

上記の例では、すべてのelse分岐が単一のステートメント(ifステートメント)であることに注意してください。else分岐にブロックのみを許可するプログラミング言語では、連鎖のために何らかのelse-if分岐が必要です。

落とし穴:ぶら下がりelse

次の例のelse分岐は、2つのifステートメントのどちらに属しているかが明確ではないため、ぶら下がり(dangling)と呼ばれます。

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

簡単なルールは次のとおりです。中括弧を使用します。上記のコードスニペットは、次のコードと同等です(elseが誰に属しているかが明らかです)。

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

switch

switchステートメント:

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

expressionを評価し、結果と一致するラベルを持つcase句にジャンプします。一致するラベルがない場合、switchは、default句が存在する場合はそこにジャンプし、そうでない場合は何も行いません。

caseの後の「オペランド」は任意の式です。===を使用してswitchのパラメータと比較されます。

句を終了ステートメントで終了しない場合、実行は次の句に続きます。最も頻繁に使用される終了ステートメントはbreakです。しかし、returnthrowも機能します。通常はswitchステートメントだけを終了するわけではありませんが。

次の例は、throwまたはreturnを使用する場合、breakする必要がないことを示しています。

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

この例では、default句はありません。したがって、fruitがいずれのcaseラベルとも一致しない場合、何も起こりません。

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

ここでは、複数のcaseラベルが連続しています。

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);
}

この例は、caseの後の値が任意の式になることを示しています。

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

上記のswitchステートメントは、case句を調べて、パラメータtrueと一致するものを探します。case式のいずれかがtrueと評価された場合、対応するcase本文が実行されます。したがって、上記のコードは次のifステートメントと同等です。

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

通常は後者の解決策の方が望ましいです。より分かりやすいです。

withステートメント

このセクションでは、JavaScriptでwithステートメントがどのように機能するか、そしてなぜその使用が推奨されないのかを説明します。

構文とセマンティクス

withステートメントの構文は次のとおりです。

with («object»)
    «statement»

objectのプロパティを、statementのローカル変数に変換します。次に例を示します。

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

その使用目的は、オブジェクトに複数回アクセスする際の冗長性を回避することです。冗長性のあるコードの例を次に示します。

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

withを使用すると、これが短くなります。

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

withステートメントは非推奨

withステートメントの使用は一般的に推奨されません(次のセクションでその理由を説明します)。たとえば、厳格モードでは禁止されています。

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

withステートメントを回避するためのテクニック

次のようなコードは避けてください。

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

代わりに、短い名前の一時変数を使用します。

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

一時変数bを現在のスコープに公開したくない場合は、IIFEを使用できます(IIFEによる新しいスコープの導入を参照)。

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

アクセスしたいオブジェクトをIIFEのパラメータにすることもできます。

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

非推奨の理由

withが非推奨とされる理由を理解するために、次の例を見て、関数の引数がどのように動作を完全に変えるかを確認してください。

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

optsにプロパティmsgがある場合、行(1)のステートメントはパラメータmsgにアクセスしなくなります。プロパティにアクセスします。

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

withステートメントによって発生する問題は3つあります。

パフォーマンスの低下
オブジェクトが一時的にスコープチェーンに挿入されるため、変数のルックアップが遅くなります。
コードの予測可能性の低下

構文上の周囲(レキシカルコンテキスト)を見るだけでは、識別子が何を指しているかを判断できません。Brendan Eichによると、withが非推奨とされたのは、パフォーマンスの考慮ではなく、これが実際の理由でした。

withはレキシカルスコープに違反するため、プログラム分析(セキュリティなど)が困難または不可能になります。

ミニファイア(第32章で説明)は変数名を短縮できません。
with文の中では、名前が変数を参照しているのかプロパティを参照しているのかを静的に判断できません。ミニファイアで名前を変更できるのは変数だけです。

withによってコードが脆弱になる例を次に示します。

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

配列myDataにアクセスできなくても、(3)行目の関数呼び出しが機能しないようにすることができます。

どのようにするのでしょうか? Array.prototypeにプロパティvaluesを追加することで可能です。 例えば、

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

これで、(2)行目のコードは、values.someMethod()ではなく、someArray.values.someMethod()を呼び出すようになります。その理由は、with文の中では、valuesが(1)行目のローカル変数ではなく、someArray.valuesを参照するようになるからです。

これは単なる思考実験ではありません。配列メソッドvalues()がFirefoxに追加されたことで、コンテンツ管理システムTYPO3に障害が発生しました。Brandon Benvie氏が問題の原因を突き止めました

debugger文

debuggerの構文は次のとおりです。

debugger;

デバッガがアクティブな場合、この文はブレークポイントとして機能します。アクティブでない場合は、目に見える効果はありません。

次: 14. 例外処理