ループと反復処理

プログラミングにおいて、同じ処理を繰り返すために同じコードを書く必要はありません。 ループや再帰呼び出し、イテレータなどを使い、反復処理は抽象化します。 ここでは、もっとも基本的な反復処理と制御文について学んでいきます。

while文

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

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

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

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

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

let x = 0;
while (x < 10) {
    console.log(x);
    x += 1;
}

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

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

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; // 初期値は0
for (let i = 0; i < 10; i++) {
    total += i + 1; // 1...10
}
console.log(total); // => 55

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

次のコードでは、任意の数値が入った配列を受け取り、その合計値を返す sum 関数を実装しています。 関数とブロック文それぞれのスコープがあるので、varletに書き換えると間違って同じ変数名を再定義できなくなるのでより安全です。

  • [ ] TODO: スコープの説明がない

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

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

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

Array#forEach

Arrayオブジェクトは、mapreduceなどの反復処理のためのメソッドが用意されています。 forEachメソッドもそのひとつでfor文に近い反復処理を行います。

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

const array = [1, 2, 3, 4, 5];
array.forEach((currentValue, index, array) => {
    // 実行する文
});

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

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

const array = [1, 2, 3, 4, 5];
array.forEach(コールバック関数);

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

[1, 2, 3].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文よりもシンプルな処理です。

break文

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

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

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

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

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

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

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

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

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

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

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

Array#some

先ほどの isEventIncludedは、偶数を見つけたら true を返す関数でした。 Arrayオブジェクトでは、someメソッドで同様のことが行えます。

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

const array = [1, 2, 3, 4, 5];
const isPassed = array.some((currentValue, index, array) => {
    // テストをパスする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文は処理中の文をスキップして、そのループの条件式と移行する制御文です。 while、do-while、forの中で使い、実行中のループの条件式へ制御を移します。

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]

もちろん次のように、偶数ならresultsへ追加するという書き方も可能です。

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

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

Array.prototype.filter

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

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

const array = [1, 2, 3, 4, 5];
// テストをパスしたものを集めた配列
const filterdArray = 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文は、対象となるオブジェクトのプロパティを列挙する場合、すべての親オブジェクトまで探索し列挙します。 そのため、オブジェクト自身が持っていないプロパティも列挙されてしまうことがあります。

この仕組みをプロトタイプチェーンといいますが、詳しくは第n章で解説します。

安全にオブジェクトのプロパティを列挙するには、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など、iterableなオブジェクトとして実装されているものは多いです。 for...of文はそれらに対して反復処理を行うことができます。

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

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

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

反復処理により新しい値を作るArrayメソッドとしてArray#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と同じく、一度作った変数の値を変更しないため、意図しない変数の更新を避けることにつながります。

results matching ""

    No results matching ""