ループと反復処理

プログラミングにおいて、同じ処理を繰り返すために同じコードを繰り返し書く必要はありません。 ループやイテレータなどを使い、反復処理として同じ処理を繰り返し実行できます。 この章では、while文やfor文などの基本的な反復処理と制御文について学んでいきます。

また、for文などのような構文だけではなく、配列のメソッドを利用して反復処理を行う方法もあります。 配列のメソッドを使った反復処理もよく利用されるため、合わせてみていきます。

while文

while文は条件式trueであるならば、反復処理を行います。

while (条件式) {
    実行する文;
}

while文の実行フローは次のようになります。 最初から条件式falseである場合は、何も実行せずwhile文は終了します。

  1. 条件式 の評価結果がtrueなら処理を続け、falseなら終了
  2. 実行する文を実行
  3. ステップ1へ戻る

次のコードではxの値が10未満であるなら、コンソールへ繰り返しログが出力されます。 また、実行する文にて、xの値を増やし条件式falseとなるようにしています。

let x = 0;
console.log(`ループ開始前のxの値: ${x}`);
while (x < 10) {
    console.log(x);
    x += 1;
}
console.log(`ループ終了後のxの値: ${x}`);

つまり、実行する文の中で条件式falseとなるような処理を書かないと無限ループします。 JavaScriptにはより安全な反復処理の書き方があるため、while文は使う場面が限られています。

安易にwhile文を使うよりも、他の書き方で解決できないかを考えてからでも遅くはないでしょう。

[コラム] 無限ループ

反復処理を扱う際に、コードの書き間違いや条件式のミスなどから無限ループを引き起こしてしまう場合があります。 たとえば、次のコードは条件式の評価結果が常にtrueとなってしまうため、無限ループが発生してしまいます。

let i = 1;
// 条件式が常にtrueになるため、無限ループする
while (i > 0) {
    console.log(`${i}回目のループ`);
    i += 1;
}

無限ループが発生してしまったときは、あわてずにスクリプトを停止してからコードを修正しましょう。

ほとんどのブラウザは無限ループが発生した際に、自動的にスクリプトの実行を停止する機能が含まれています。 また、ブラウザで該当のスクリプトを実行しているページ(タブ)またはブラウザそのものを閉じることで強制的に停止できます。 Node.jsで実行している場合はCtrl + Cを入力し、終了シグナルを送ることで強制的に停止できます。

無限ループが発生する原因のほとんどは条件式に関連する実装ミスです。 まずは条件式の確認をしてみることで問題を解決できるはずです。

do-while文

do-while文はwhile文と殆ど同じですが実行順序が異なります。

do {
    実行する文;
} while (条件式);

do-while文の実行フローは次のようになります。

  1. 実行する文を実行
  2. 条件式 の評価結果がtrueなら処理を続け、falseなら終了
  3. ステップ1へ戻る

while文とは異なり、かならず最初に実行する文を処理します。

そのため、次のコードのように最初から条件式を満たさない場合でも、 初回の実行する文が処理され、コンソールへ1000と出力されます。

const x = 1000;
do {
    console.log(x); // => 1000
} while (x < 10);

この仕組みを上手く利用し、ループの開始前とループ中の処理をまとめて書くことができます。 しかし、while文と同じく他の書き方で解決できないかを考えてからでも遅くはないでしょう。

for文

for文は繰り返す範囲を指定した反復処理を書くことができます。

for (初期化式; 条件式; 増分式) {
    実行する文;
}

for文の実行フローは次のようになります。

  1. 初期化式 で変数の宣言
  2. 条件式 の評価結果がtrueなら処理を続け、falseなら終了
  3. 実行する文 を実行
  4. 増分式 で変数を更新
  5. ステップ2へ戻る

次のコードでは、for文で1から10までの値を合計して、その結果をコンソールへ出力しています。

let total = 0; // totalの初期値は0
// for文の実行フロー
// iを0で初期化
// iが10未満(条件式を満たす)ならfor文の処理を実行
// iに1を足し、再び条件式の判定へ
for (let i = 0; i < 10; i++) {
    total += i + 1; // 1から10の値をtotalに加算している
}
console.log(total); // => 55

このコードは1から10までの合計を電卓で計算すればいいので、普通は必要ありませんね。 もう少し実用的なものを考えると、任意の数値の入った配列を受け取り、その合計を計算して返すという関数を実装すると良さそうです。

次のコードでは、任意の数値が入った配列を受け取り、その合計値を返す sum 関数を実装しています。

function sum(numbers) {
    let total = 0;
    for (let i = 0; i < numbers.length; i++) {
        total += numbers[i];
    }
    return total;
}

console.log(sum([1, 2, 3, 4, 5])); // => 15

ここまで見てきたように反復処理の多くは、配列に入れた値を処理する方法と言いかえることができます。 JavaScriptの配列であるArrayオブジェクトには、反復処理のためのメソッドが備わっています。 そのため、配列のメソッドを使った反復処理も合わせて見ていきます。

配列のforEachメソッド

配列にはforEachメソッドというfor文と同じように反復処理を行うメソッドがあります。

forEachメソッドでの反復処理は、次のように書くことができます。

const array = [1, 2, 3];
array.forEach(currentValue => {
    // ループごとに実行する処理
});

JavaScriptでは、関数がファーストクラスであるため、その場で作った匿名関数(名前のない関数)を引数として渡すことができます。

引数として渡される関数のことをコールバック関数と呼びます。 また、コールバック関数を引数として受け取る関数やメソッドのことを高階関数と呼びます。

const array = [1, 2, 3];
// forEachは"コールバック関数"を受け取る高階関数
array.forEach(コールバック関数);

forEachメソッドのコールバック関数には、配列の要素が先頭から順番に渡されて実行されます。 つまり、コールバック関数のcurrentValueには1から3の値が順番に渡されます。

const array = [1, 2, 3];
array.forEach(currentValue => {
    console.log(currentValue);
});
// 1
// 2
// 3
// と順番に出力される

先ほどのfor文の例と同じ数値の合計を返すsum 関数をforEachメソッドで実装してみます。

function sum(numbers) {
    let total = 0;
    numbers.forEach(num => {
        total += num;
    });
    return total;
}

sum([1, 2, 3, 4, 5]); // => 15

forEachはfor文の条件式に相当するものはなく、必ず配列のすべての要素を反復処理します。 変数iといった一時的な値を定義する必要がないため、シンプルに反復処理を書けます。

break文

break文は処理中の文から抜けて次の文へ移行する制御文です。 while、do-while、forの中で使い、処理中のループを抜けて次の文へ制御を移します。

while (true) {
    break; // *1 へ
}
// *1 次の文

switch文で出てきたものと同様で、処理中のループ文を終了できます。

次のコードでは配列の要素に1つでも偶数を含んでいるかを判定しています。

const numbers = [1, 5, 10, 15, 20];
// 偶数があるかどうか
let isEvenIncluded = false;
for (let i = 0; i < numbers.length; i++) {
    const number = numbers[i];
    if (number % 2 === 0) {
        isEvenIncluded = true;
        break;
    }
}
console.log(isEvenIncluded); // => true

1つでも偶数があるかが分かればいいため、配列内から最初の偶数を見つけたらfor文での反復処理を終了します。 このような処理はベタ書きせずに、関数として実装するのが一般的です。

同様の処理をする isEvenIncluded 関数を実装してみます。 次のコードでは、break文が実行され、ループを抜けた後にreturn文で結果を返しています。

// `number`が偶数ならtrueを返す
function isEven(number) {
    return number % 2 === 0;
}
// `numbers`に偶数が含まれているならtrueを返す
function isEvenIncluded(numbers) {
    let isEvenIncluded = false;
    for (let i = 0; i < numbers.length; i++) {
        const number = numbers[i];
        if (isEven(number)) {
            isEvenIncluded = true;
            break;
        }
    }
    return isEvenIncluded;
}
const array = [1, 5, 10, 15, 20];
console.log(isEvenIncluded(array)); // => true

return文は現在の関数を終了させることができるため、次のように書くこともできます。

function isEven(number) {
    return number % 2 === 0;
}
function isEvenIncluded(numbers) {
    for (let i = 0; i < numbers.length; i++) {
        const number = numbers[i];
        if (isEven(number)) {
            return true;
        }
    }
    return false;
}
const numbers = [1, 5, 10, 15, 20];
console.log(isEvenIncluded(numbers)); // => true

偶数を見つけたらすぐにreturnすることで一時変数が不要となり、より簡潔に書くことができます。

配列のsomeメソッド

先ほどのisEvenIncluded関数は、偶数を見つけたら true を返す関数でした。 配列ではsomeメソッドで同様のことが行えます。

someメソッドは、配列の各要素をテストする処理をコールバック関数として受け取ります。 コールバック関数が、一度でもtrueを返した時点で反復処理を終了し、someメソッドはtrueを返します。

const array = [1, 2, 3, 4, 5];
const isPassed = array.some(currentValue => {
    // テストをパスするtrue、そうでないならfalseを返す
});

someメソッドを使うことで、配列に偶数が含まれているかは次のように書くことができます。 受け取った値が偶数であるかをテストするコールバック関数としてisEven関数を渡します。

function isEven(number) {
    return number % 2 === 0;
}
const numbers = [1, 5, 10, 15, 20];
console.log(numbers.some(isEven)); // => true

continue文

continue文は現在の反復処理を終了して、次の反復処理を行います。 continue文は、while、do-while、forの中で使うことができます。

たとえば、while文の処理中でcontinue文が実行されると、現在の反復処理はその時点で終了されます。 そして、次の反復処理で条件式を評価するところからループが再開されます。

while (条件式) {
    // 実行される処理
    continue; // `条件式` へ
    // これ以降の行は実行されません
}

次のコードでは、配列の中から偶数を集め、新しい配列を作り返しています。 偶数ではない場合、処理中のfor文をスキップしています。

// `number`が偶数ならtrueを返す
function isEven(number) {
    return number % 2 === 0;
}
// `numbers`に含まれている偶数だけを取り出す
function filterEven(numbers) {
    const results = [];
    for (let i = 0; i < numbers.length; i++) {
        const number = numbers[i];
        // 偶数ではないなら、次のループへ
        if (!isEven(number)) {
            continue;
        }
        // 偶数を`results`に追加
        results.push(number);
    }
    return results;
}
const array = [1, 5, 10, 15, 20];
console.log(filterEven(array)); // => [10, 20]

もちろん、次のようにcontinue文を使わずに「偶数ならresultsへ追加する」という書き方も可能です。

if (isEven(number)) {
    results.push(number);
}

この場合、条件が複雑になってきた場合にネストが深くなってコードが読みにくくなります。 そのため、ネストしたif文のうるう年の例でも紹介したように、 できるだけ早い段階でそれ以上処理を続けない宣言をすることで、複雑なコードになることを避けています。

配列のfilterメソッド

配列から特定の値だけを集めた新しい配列を作るにはfilterメソッドを利用できます。

filterメソッドには、配列の各要素をテストする処理をコールバック関数として渡します。 コールバック関数がtrueを返した要素のみを集めた新しい配列を返します。

const array = [1, 2, 3, 4, 5];
// テストをパスしたものを集めた配列
const filteredArray = array.filter((currentValue, index, array) => {
    // テストをパスするならtrue、そうでないならfalseを返す
});

このfilterメソッドを使うことで、次のように偶数だけに絞り込む処理を書けます。

function isEven(number) {
    return number % 2 === 0;
}

const array = [1, 5, 10, 15, 20];
console.log(array.filter(isEven)); // => [10, 20]

for...in文

for...in文はオブジェクトのプロパティに対して、順不同で反復処理を行います。

for (variable in object) {
    実行する文;
}

次のコードではobjectのプロパティ名をkey変数に代入し反復処理をしています。 objectには、3つのプロパティ名があるため3回繰り返されます。

const object = {
    "a": 1,
    "b": 2,
    "c": 3
};
for (const key in object) {
    const value = object[key];
    console.log(`key:${key}, value:${value}`);
}
// "key:a, value:1"
// "key:b, value:2"
// "key:c, value:3"

オブジェクトに対する反復処理のためにfor...in文は有用に見えますが、多くの問題を持っています。

JavaScriptでは、オブジェクトは何らかのオブジェクトを継承しています。 for...in文は、対象となるオブジェクトのプロパティを列挙する場合に、親オブジェクトまで列挙可能なものがあるかを探索して列挙します。 そのため、オブジェクト自身が持っていないプロパティも列挙されてしまい、意図しない結果になる場合があります。

安全にオブジェクトのプロパティを列挙するには、Object.keysメソッド、Object.valuesメソッド、Object.entriesメソッドなどが利用できます。

先ほどの例である、オブジェクトのキーと値を列挙するコードはfor...in文を使わずに書けます。 Object.keysメソッドはobject自身がもつ列挙可能なプロパティ名の配列を返します。 そのためfor...in文とは違い、親オブジェクトのプロパティは列挙されません。

const object = {
    "a": 1,
    "b": 2,
    "c": 3
};
Object.keys(object).forEach(key => {
    const value = object[key];
    console.log(`key:${key}, value:${value}`);
});
// "key:a, value:1"
// "key:b, value:2"
// "key:c, value:3"

また、for...in文は配列に対しても利用できますが、こちらも期待した結果にはなりません。

次のコードでは、配列の要素が列挙されそうですが、実際には配列のプロパティ名が列挙されます。 for...in文が列挙する配列オブジェクトのプロパティ名は、要素のインデックスを文字列化した"0"、"1"となるため、その文字列がnumへと順番に代入されます。 そのため、数値と文字列の加算が行われ、意図した結果にはなりません。

const numbers = [5, 10];
let total = 0;
for (const num in numbers) {
    total += num;
}
console.log(total); // => "001"

配列の内容に対して反復処理を行う場合は、for文やforEachメソッド、後述するfor...of文を使うべきでしょう。

このようにfor...in文は正しく扱うのが難しいですが、代わりとなる手段が豊富にあります。 そのため、for...in文の利用は避け、他の方法を考えた方がよいでしょう。

[ES2015] for...of文

最後にfor...of文についてです。

JavaScriptでは、Symbol.iteratorという特別な名前のメソッドを実装したオブジェクトをiterableと呼びます。 iterableオブジェクトは、for...of文で反復処理できます。

iterableについてはgeneratorと密接な関係がありますが、ここでは反復処理時の動作が定義されたオブジェクトと認識していれば問題ありません。

iterableオブジェクトは反復処理時に次の返す値を定義しています。 それに対して、for...of文では、iterableから値を1つ取り出し、variableに代入し反復処理を行います。

for (variable of iterable) {
    実行する文;
}

実はすでにiterableオブジェクトは登場していて、Arrayはiterableオブジェクトです。

次のようにfor...of文で、配列から値を取り出し反復処理を行うことができます。 for...in文とは異なり、インデックス値ではなく配列の値を列挙します。

const array = [1, 2, 3];
for (const value of array) {
    console.log(value);
}
// 1
// 2
// 3

JavaScriptではStringオブジェクトもiterableです。 そのため、文字列を1文字ずつ列挙できます。

const string = "𠮷野家";
for (const value of string) {
    console.log(value);
}
// "𠮷"
// "野"
// "家"

その他にも、TypedArrayMapSet、DOM NodeListなど、Symbol.iteratorを実装されているオブジェクトは多いです。 for...of文はそれらのiterableオブジェクトを反復処理できます。

[コラム] letではなくconstで反復処理をする

先ほどのfor文やforEachメソッドではletconstに変更できませんでした。 なぜなら、for文は一度定義した変数に値の代入を繰り返し行う処理といえるからです。 const は再代入できない変数を宣言するキーワードであるためfor文とは相性がよくありません。

一度定義した変数に値を代入しつつ反復処理すると、変数へ値の上書きが必要となりconstを使うことができません。 そのため、一時的な変数を定義せずに反復処理した結果だけを受け取る方法が必要になります。

配列には、反復処理をして新しい値を作るreduceメソッドがあります。

reduceメソッドは2つずつ要素を取り出し(左から右へ)、その値をコールバック関数に適用し、 次の値として1つの値を返します。 最終的な、reduceメソッドの返り値は、コールバック関数が最後にreturnした値となります。

const result = array.reduce((前回の値, 現在の値) => {
    return 次の値;
}, 初期値);

配列から合計値を返すものをreduceメソッドを使い実装してみましょう。

先ほどの配列の全要素の合計値を計算するものはreduceメソッドでは、次のように書くことができます。 初期値0を指定し、前回の値現在の値を足していくことで合計を計算できます。 初期値を指定していた場合は、最初の前回の値に初期値が、配列の先頭の値が現在の値となった状態で開始されます。

function sum(numbers) {
    return numbers.reduce((total, num) => {
        return total + num;
    }, 0); // 初期値が0
}

sum([1, 2, 3, 4, 5]); // => 15

reduceメソッドを使った例では、そもそも変数宣言をしていないことが分かります。 reduceメソッドでは常に新しい値を返すことで、1つの変数の値を更新していく必要がなくなります。 これはconstと同じく、一度作った変数の値を変更しないため、意図しない変数の更新を避けることにつながります。

まとめ

この章では、for文などの構文での反復処理と配列のメソッドを使った反復処理について比較しながら見ていきました。 for文などの構文ではcontinue文やbreak文が利用できますが、配列のメソッドではそれらは利用できません。 一方で配列のメソッドは、一時的な変数を管理する必要がないことや、処理をコールバック関数として書くという違いがあります。

どちらの方法も反復処理においてはよく利用されます。 どちらが優れているというわけでもないため、どちらの方法も使いこなせるようになることが重要です。 また、配列のメソッドについては「配列」の章でも詳しく解説します。