2023年2月8日

ループ: while と for

繰り返し処理は頻繁に必要になります。

例えば、1つずつリストから商品を出力するような場合や、単に1から10の数値それぞれに同じコードを実行する場合などです。

ループ は複数回同じコードを繰り返す手段です。

for…of と for…in ループ

上級者向けのちょっとしたお知らせです。

この記事は基本的なループ: while, do..whilefor(..;..;..) のみの説明をしています。

他の種類のループを検索してこの記事にたどり着いたのであれば、以下を参照してください。

  • オブジェクトプロパティのループは for…in です。
  • 配列や反復可能オブジェクトに対するループは for…ofiterables です。

それ以外の場合は、続けて読んでみてください。

“while” ループ

while ループは次の構文になります:

while (condition) {
  // code
  // いわゆる "ループ本体" です
}

conditiontrue の間、ループの本体の code が実行されます。

例えば、以下のループは i < 3 の間、i を出力します:

let i = 0;
while (i < 3) { // 0, 次に 1, 次に 2 を表示
  alert( i );
  i++;
}

ループ本体の1回の実行は イテレーション と呼ばれます。上の例のループは3回イテレーションします。

もしも上の例に i++ がない場合、ループは (理論上は) 永遠に繰り返されます。実際には、ブラウザはこのようなループを止める方法を提供しており、サーバサイドJavaScriptではそのプロセスを殺すことができます。

比較に限らず、どんな式や変数もループの条件にすることができます。条件は while によって評価され、真偽値に変換されます。

たとえば、while (i != 0) をより短く書く方法としてwhile (i)があります:

let i = 3;
while (i) { // i が 0 になったとき、条件が偽になり、ループが止まります
  alert( i );
  i--;
}
本体が1行の場合、括弧は必須ではありません

ループの本体が単一の文である場合、括弧{…}を省略することができます:

let i = 3;
while (i) alert(i--);

“do…while” ループ

do..while 構文を使うことで、条件チェックをループ本体の 下に 移動させることができます。:

do {
  // loop body
} while (condition);

ループは最初に本体を実行した後、条件をチェックし、条件が真である間、本体の実行を繰り返します。

例:

let i = 0;
do {
  alert( i );
  i++;
} while (i < 3);

この構文の形式は、条件が真になるかどうかに関わらず、少なくとも1度 はループ本体を実行したい場合にのみ使用されるべきです。通常は他の形式が好まれます: while(…) {…}

“for” ループ

for ループは最も使われるものの1つです。

このようになります:

for (begin; condition; step) {
  // ... loop body ...
}

例でこれらのパーツの意味を学びましょう。下のループは i0 から 3 になるまで(3 は含みません)、 alert(i) を実行します。:

for (let i = 0; i < 3; i++) { // 0, 次に 1, 次に 2 を表示
  alert(i);
}

for 文を部分的に調べてみましょう:

パート
begin i = 0 ループに入ると1度実行されます。
condition i < 3 すべてのループのイテレーションの前にチェックされます。条件が偽の場合、ループを停止します。
step i++ 各イテレーションで、条件チェックの前に本体の後に実行されます。
body alert(i) 条件が真の間繰り返し実行されます。

一般的なループアルゴリズムは次のように動作します:

begin を実行
→ (if condition → body を実行し step を実行)
→ (if condition → body を実行し step を実行)
→ (if condition → body を実行し step を実行)
→ ...

つまり、begin を1度実行し、その後、各 condition の評価の後、bodystep が実行されるという繰り返しになります。

もしループに慣れていない場合は、上の例に戻って、紙の上でステップ毎にどのように動作するかを再現してみると理解しやすいでしょう。

これが今のケースで正確に起こっていることです:

// for (let i = 0; i < 3; i++) alert(i)

// begin を実行
let i = 0
// if condition → body を実行し step を実行
if (i < 3) { alert(i); i++ }
// if condition → body を実行し step を実行
if (i < 3) { alert(i); i++ }
// if condition → body を実行し step を実行
if (i < 3) { alert(i); i++ }
// ...終わり, 今 i == 3 なので
インライン変数宣言

ここで “カウンタ” 変数 i はループの中で正しく宣言されます。それは “インライン” 変数宣言と呼ばれます。このような変数はループの中でだけ見えます。

for (let i = 0; i < 3; i++) {
  alert(i); // 0, 1, 2
}
alert(i); // エラー, そのような変数はありません

変数を宣言する代わりに、既存のものを使うこともできます:

let i = 0;

for (i = 0; i < 3; i++) { // 既存の変数を使用
  alert(i); // 0, 1, 2
}

alert(i); // 3, ループの外で宣言されているので見える

一部分の省略

for の一部分は省略することができます。

例えば、ループの最初で何もする必要がなければ、begin を省略できます。

このようになります:

let i = 0; // すでに i を宣言し代入済み

for (; i < 3; i++) { // "begin" 不要
  alert( i ); // 0, 1, 2
}

同じように step パートも除去することができます。:

let i = 0;

for (; i < 3;) {
  alert( i++ );
}

ループは while (i < 3) と同じになりました。

実際にはすべてを除くこともできます。それは無限ループになります:

for (;;) {
  // 制限なしで繰り返し
}

for の2つのセミコロン ; は必須であることに注意してください。ない場合は構文エラーになります。

ループの終わり

通常、ループは条件が偽になると終了します。

ですが、break ディレクティブを利用して、いつでも強制的にループを終わらせることができます。

例えば、以下のループはユーザに一連の数字を入力するよう求めますが、数字が入力されなかった場合は “中断” します。:

let sum = 0;

while (true) {

  let value = +prompt("Enter a number", '');

  if (!value) break; // (*)

  sum += value;

}
alert( 'Sum: ' + sum );

もしもユーザが空を入力、もしくは入力をキャンセルした場合、break ディレクティブは行 (*) で有効になります。それはループをすぐに停止し、ループ後の最初の行へ制御を渡します。つまり、alert です。

"無限ループ + 必要に応じた break" の組み合わせは、ループの最初や最後ではなく、途中や本体の様々な場所で条件をチェックする必要がある状況で最適です。

次のイテレーションに進む

continue ディレクティブは break の “軽量版” です。ループ全体はストップしません。その代わりに、現在のイテレーションを停止し、新しいイテレーションのスタートを強制します(条件が真であれば)。

現在のイテレーションが完了し、次へ移動したいときに使います。

以下のループは、奇数値のみを出力するよう continue を使用しています:

for (let i = 0; i < 10; i++) {

  // true の場合、本体の残りのパートをスキップ
  if (i % 2 == 0) continue;

  alert(i); // 1, 次に 3, 5, 7, 9
}

i の値が偶数の場合、continue ディレクティブは本体の実行を停止し、for の次のイテレーションへ制御を渡します(次の番号で)。したがって、alert は奇数値に対してのみ実行されます。

ディレクティブ continue を使うと入れ子のレベルを減らせます

奇数値を表示するループはこのように書くこともできます:

for (let i = 0; i < 10; i++) {

  if (i % 2) {
    alert( i );
  }

}

技術的な観点からは、これは上の例と同じです。確かに、continue の代わりに if ブロックでコードをラップするだけです。

しかし、副作用として括弧のネストが1段深くなります。if の中のコードが長い場合、全体の可読性が下がる可能性があります。

? の右側には break/continue を入れないでください

式ではない構文構造は、 三項演算子 ? の中では使えないことに注意してください。特に、ディレクティブ break/continue はそこでは許可されません。

例えば、次のようなコードがあるとします:

if (i > 5) {
  alert(i);
} else {
  continue;
}

…これを、疑問符を使って書き直します:

(i > 5) ? alert(i) : continue; // continue はここでは使えません

…この場合は動作を停止します。コードは構文エラーになります:

これは if の代わりに疑問符演算子 ? を使用しない別の理由です。

break/continue のためのラベル

一度に複数のネストしたループから抜け出すことが必要となる場合があります。

例えば、下のコードでは 座標 (i, j)(0,0) から (2,2) へプロンプトするよう、ij をループします:

for (let i = 0; i < 3; i++) {

  for (let j = 0; j < 3; j++) {

    let input = prompt(`Value at coords (${i},${j})`, '');

    // もしここで終了して下にある Done をしたい場合にはどうすればよいでしょう?
  }
}

alert('Done!');

ユーザが入力をキャンセルした場合、処理をストップする方法が必要です。

input の後の通常の break は内部ループのみの終了です。それだけでは十分ではありません。ここでラベルが救いの手を差し伸べてくれます。

ラベル は、ループの前のコロンがついた識別子です:

labelName: for (...) {
  ...
}

ループの中の break <labelName> 文はラベルまで抜け出します:

outer: for (let i = 0; i < 3; i++) {

  for (let j = 0; j < 3; j++) {

    let input = prompt(`Value at coords (${i},${j})`, '');

    // 文字から文字またはキャンセルされた場合、両方のループから抜ける
    if (!input) break outer; // (*)

    // 値に何かをする処理...
  }
}

alert('Done!');

上のコードで、break outerouter と名付けされたラベルを上に探し、そのループを抜けます。

そのため、制御は (*) から alert('Done!') にまっすぐに進みます。

ラベルを別の行に移動させることもできます:

outer:
for (let i = 0; i < 3; i++) { ... }

continue ディレクティブもラベルと一緒に使うことができます。このケースでは、実行はラベル付けされたループの次のイテレーションにジャンプします。

ラベルはどこにでも “ジャンプ” を許可するものではありません

ラベルはコードの任意の場所にジャンプすることはできません。

例えば、このようにすることは出来ません:

break label;  // 以下のラベルにジャンプはしません

label: for (...)

break ディレクティブはコードブロックの中にある必要があります。技術的には任意のラベル付けされたコードブロックであれば機能します。

label: {
  // ...
  break label; // works
  // ...
}

…ですが、break が利用される 99.9% は上の例で見てきたように、ループの内側です。

continue はループの内側でのみ利用可能です。

サマリ

3種類のループについて説明しました:

  • while – 条件は各イテレーションの前にチェックされます。
  • do..while – 条件は各イテレーションの後にチェックされます。
  • for (;;) – 条件は各イテレーションの前にチェックされ、追加の設定ができます。

“無限” ループを作るには、通常は while(true) 構造が使われます。このようなループは他の他のループと同様に break ディレクティブで停止することができます。

もしも現在のイテレーションで何もしたくなく、次のイテレーションに進みたい場合は、continue ディレクティブを使います。

break/continue はループの前のラベルをサポートします。ラベルは、 break/continue でネストされたループを抜けて外側のループに行くための唯一の方法です。

タスク

重要性: 3

このコードで最後にアラートされる値は何でしょう?それはなぜでしょう?

let i = 3;

while (i) {
  alert( i-- );
}

答え: 1.

let i = 3;

while (i) {
  alert( i-- );
}

各ループイテレーションは i1 減らします。チェック while(i)i = 0 のときにループを停止します。

従って、ループのステップは次のシーケンスを形成します。:

let i = 3;

alert(i--); // 3 を表示, i を 2 に減らす

alert(i--) // 2 を表示, i を 1 に減らす

alert(i--) // 1 を表示, i を 0 に減らす

// 完了。while(i)チェックでループが停止します。
重要性: 4

各ループで、どの値が表示されるか、あなたの意見を書きなさい。また、それと答えを見比べてみてください。

両方のループは同じ数だけ alert されますか?それとも違いますか?

  1. プレフィックス形式 ++i:

    let i = 0;
    while (++i < 5) alert( i );
  2. ポストフィックス形式 i++

    let i = 0;
    while (i++ < 5) alert( i );

このタスクは、ポストフィックス/サフィックス形式を比較で使ったときに、どのように異なる結果に繋がるかを示します。

  1. 1 から 4

    let i = 0;
    while (++i < 5) alert( i );

    最初の値は i=1 です。なぜなら、i++ は最初に i をインクリメントし、新しい値を返します。なので、最初の比較は 1 < 5 で、alert1 を表示します。

    次に、2,3,4… に続きます – 値は次々に表示されます。比較は常にインクリメントされた値を使います。なぜなら ++ は変数の前にあるからです。

    最終的に、i=4 では 5 にインクリメントされ、比較 while(5 < 5) が偽になりループが停止します。なので、5 は表示されません。

  2. 1 から 5

    let i = 0;
    while (i++ < 5) alert( i );

    最初の値は再び i=1 です。i++ のポストフィックス形式は i をインクリメントし、古い 値を返します。なので、比較 i++ < 5i=0 を使います (++i < 5 とは逆です)。

    しかし、alert 呼び出しは別です。インクリメントと比較の後に実行される別の文なので、現在の i=1 を使います。

    そして 2,3,4… に続きます。

    i=4 で止めてみましょう。プレフィックス形式 ++i はインクリメントし、比較では 5 を使います。しかし、ここではポストフィックスです。なので、i5 にインクリメントしますが、古い値を返します。従って、比較は実際 while(4 < 5) – true です。そして制御は alert に行きます。

    i=5 は最後です。なぜなら次のステップ while(5 < 5) は偽になるからです。

重要性: 4

各ループでどの値が表示されるか書き留めてください。そして答えと比較してください。

両ループ同じ値を alert しますか?それとも違いますか?

  1. ポストフィックス形式:

    for (let i = 0; i < 5; i++) alert( i );
  2. プレフィックス形式:

    for (let i = 0; i < 5; ++i) alert( i );

答え: どちらも場合も 0 から 4 です

for (let i = 0; i < 5; ++i) alert( i );

for (let i = 0; i < 5; i++) alert( i );

これは for のアルゴリズムから簡単に差し引くことができます:

  1. すべての前(最初)に i = 0 を一度実行します。
  2. 条件 i < 5 をチェックします。
  3. もし true なら – ループ本体 alert(i) を実行し、i++ します。

インクリメント i++ は条件チェック (2) とは分離されています。それは単に別の文です。

インクリメントによって返された値はここでは使われていません。なので、 i++++i の間に違いはありません。

重要性: 5

for ループを使って 2 から 10 までの偶数を出力してください。

デモを実行

for (let i = 2; i <= 10; i++) {
  if (i % 2 == 0) {
    alert( i );
  }
}

ここでは、残りを取得するための “剰余” 演算子 % を使って偶数のチェックをしています。

重要性: 5

その振る舞いを変えず(出力は同じまま)に、for ループから while にコードを書き換えてください。

for (let i = 0; i < 3; i++) {
  alert( `number ${i}!` );
}
let i = 0;
while (i < 3) {
  alert( `number ${i}!` );
  i++;
}
重要性: 5

100 より大きい数値を入力するプロンプトを書いてください。もし訪問者が別の数値を入力したら – 再度、入力を促します。

ループは、訪問者が 100 より大きい値を入力するか、入力をキャンセル/空行の入力をするまで訪ねます。

ここでは、訪問者は数値のみを入力すると仮定します。このタスクでは、非数値に対する特別な処理を実装する必要はありません。

デモを実行

let num;

do {
  num = prompt("Enter a number greater than 100?", 0);
} while (num <= 100 && num);

ループ do..while は両方のチェックが真になるまで繰り返します。:

  1. num <= 100 のチェック – つまり、入力値がまだ 100 よりも大きくない。
  2. && num チェックは、numnull または空文字の場合に false です。そのとき、while ループも停止します。

P.S. numnull の場合、num <= 100true なので、2回目のチェックがなければ、ユーザーがCANCELをクリックするとループは止まらなくなります。 両方のチェックが必要です。

重要性: 3

1 よりも大きい整数で、1 と自身以外では、余りなく割ることができない場合、その数値は 素数(prime) と呼ばれます。

つまり、1より大きいnが 1n 以外では割り切れない場合、素数となります。

例えば、5 は素数です。なぜなら、2, 34 ではあまり無く割ることができなからです。

2 から n の範囲で、素数を出力するコードを書きなさい。

n = 10 の場合、結果は 2,3,5,7 です。

P.S. コードは任意の n で動作させてください。固定値でハードコードはしないでください。

このタスクを解決するのに、多くのアルゴリズムがあります。

入れ子ループを使ってみましょう:

For each i in the interval {
  check if i has a divisor from 1..i
  if yes => the value is not a prime
  if no => the value is a prime, show it
}

ラベルを使ったコードです。:

let n = 10;

nextPrime:
for (let i = 2; i <= n; i++) { // for each i...

  for (let j = 2; j < i; j++) { // look for a divisor..
    if (i % j == 0) continue nextPrime; // not a prime, go next i
  }

  alert( i ); // a prime
}

ここには最適化の余地が沢山あります。例えば、2 から i の平方根までの約数を探すことができます。しかし、とにかく、私たちが大きな間隔に対して効率的になりたいなら、アプローチを変更し、高度な数学とQuadratic sieve, General number field sieveなどの複雑なアルゴリズムに頼る必要があります。

チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。