本文へスキップ
ウェブエンジニア問題集
第4章

制御構文 — if・switch・for・whileの実務パターン

約15分
この章の目次開く

JavaScriptの制御構文は、コードの実行順序を変えるための仕組みです。条件によって処理を分岐したり、繰り返したり、途中で抜け出したりできます。

この章では実務でよく使うパターンを中心に整理します。

学習者学習者

if と switch はどう使い分けるの?break や return を書き忘れてバグを出したことがある…。


条件分岐:if / else

最も基本的な制御構文です。条件が truthy か falsy かによって、実行するブロックを切り替えます。

const score = 75;
 
if (score >= 90) {
  console.log('優');
} else if (score >= 70) {
  console.log('良'); // こちらが実行される
} else {
  console.log('可');
}
js

早期 return パターン

ネストが深くなりがちな条件分岐は、**早期 return(Guard Clause)**で扁平にできます。

// ネストが深くなるパターン
function processOrder(order) {
  if (order) {
    if (order.isPaid) {
      if (order.items.length > 0) {
        // 本来の処理
        ship(order);
      }
    }
  }
}
 
// 早期 return で扁平にしたパターン
function processOrder(order) {
  if (!order) return;
  if (!order.isPaid) return;
  if (order.items.length === 0) return;
 
  // 本来の処理
  ship(order);
}
js

条件分岐:switch

複数の値と比較するときは switch が読みやすくなります。

const day = 'Monday';
 
switch (day) {
  case 'Saturday':
  case 'Sunday':
    console.log('週末');
    break;
  case 'Monday':
    console.log('月曜日'); // こちらが実行される
    break;
  default:
    console.log('平日');
}
js

フォールスルーに注意

break を書き忘れると、次の case の処理も続けて実行されます(フォールスルー)。

switch (status) {
  case 'pending':
    console.log('pending');
    // break がないと次の case も実行される!
  case 'active':
    console.log('active'); // status が 'pending' のときも実行される
    break;
}
js

意図的なフォールスルー(複数の値に同じ処理)以外は、必ず break を書きます。

オブジェクトマッピングで switch を置き換える

switch 文が単純な値の対応付けをしているだけなら、オブジェクト(マップ) で書き換えると簡潔になります。

// switch を使うパターン
function getStatusLabel(status) {
  switch (status) {
    case 'draft': return '下書き';
    case 'published': return '公開中';
    case 'archived': return 'アーカイブ';
    default: return '不明';
  }
}
 
// オブジェクトマッピングで書き換え
const STATUS_LABELS = {
  draft: '下書き',
  published: '公開中',
  archived: 'アーカイブ',
};
 
function getStatusLabel(status) {
  return STATUS_LABELS[status] ?? '不明';
}
js

マッピングをオブジェクトとして定義すると、データとロジックが分離されてテストしやすくなります。ただし、条件に複雑なロジックが含まれる場合(範囲チェック、複数条件の組み合わせなど)は if/else や switch の方が読みやすいです。


ループ:for / for...of / for...in

for 文

インデックスが必要な場合や、回数を指定して繰り返すときに使います。

for (let i = 0; i < 5; i++) {
  console.log(i); // 0, 1, 2, 3, 4
}
js

for...of

配列や文字列などのイテラブルを順に処理するときに使います。インデックスが不要なケースはこちらが読みやすいです。

const fruits = ['apple', 'banana', 'cherry'];
 
for (const fruit of fruits) {
  console.log(fruit);
}
 
// インデックスも必要な場合は entries()
for (const [index, fruit] of fruits.entries()) {
  console.log(index, fruit); // 0 'apple', 1 'banana', ...
}
js

[index, fruit] の正体は分割代入

学習者学習者

for (const fruit of fruits) は素直に読めるのに、entries() のときだけ [index, fruit] と角かっこで囲むのはなぜですか?

entries() が「インデックスと値のペア」を1つの配列として返しているからです。

ここは「entries() が何を返しているか」と「左辺の角かっこが何をしているか」の2つに分けて見ると、すっきり理解できます。

手順1:entries() は [インデックス, 値] の配列を返す

array.entries() が返すのはイテレータで、そこから順番に出てくるのは要素そのものではなく [インデックス, 値] という2要素の配列です。スプレッド構文で中身をまとめて展開すると確認できます。

const array = ['a', 'b', 'c'];
 
console.log([...array.entries()]);
// [
//   [0, 'a'],
//   [1, 'b'],
//   [2, 'c']
// ]
js

for...of が1周ごとに受け取っているのは 'a' ではなく [0, 'a'] という配列である、という点がポイントです。

手順2:左辺の角かっこは配列の分割代入

受け取るのが配列なら、entry[0] と entry[1] でも取り出せます。それを1行で書けるようにしたのが分割代入です。次の2つはまったく同じ動きをします。

// 分割代入を使わない書き方
for (const entry of array.entries()) {
  const index = entry[0];
  const item = entry[1];
  console.log(index, item);
}
 
// 分割代入を使った書き方(上と同じ意味)
for (const [index, item] of array.entries()) {
  console.log(index, item);
}
js

左辺の [index, item] は配列を作っているのではなく、右辺の配列を先頭から順に取り出して名前を付ける記法です。1周目には [0, 'a'] が渡ってくるので、index に 0、item に 'a' が入ります。

1つの配列を2つの変数に振り分けるイメージ

つまり for (const [index, item] of array.entries()) が動くのは、entries() がペアの配列を返し、それを分割代入が2つの変数へ振り分けているためです。

for...in

オブジェクトの列挙可能なプロパティキーを順に処理します。

const user = { name: 'Alice', age: 30 };
 
for (const key in user) {
  console.log(key, user[key]); // 'name' 'Alice', 'age' 30
}
js

while

条件が満たされる間、繰り返し実行します。ループに入る前に条件を判定するため、最初から条件を満たしていなければ本体は一度も実行されません。

let count = 0;
while (count < 3) {
  console.log(count); // 0, 1, 2
  count++;
}
js

do...while

本体を先に1回実行してから条件を判定します。そのため、条件を満たしていなくても最低1回は実行されます。

let n = 10;
do {
  console.log(n); // 10(条件を満たさなくても1回実行される)
  n++;
} while (n < 5);
js

for・while・do...while の使い分け

学習者学習者

for と do...while って、どっちを使えばいいのか毎回迷います…。

先生先生

実はその2つ、比べる相手が違うんです。まず「while か do...while か」で判定のタイミングを決めて、そのあと「for か while か」で書き方を決める。この2段階で考えると迷わなくなりますよ。

for と do...while は対立する選択肢ではありません。

比較すべきなのは while と do...while です。この2つの違いは条件判定が本体の前か後かという1点だけです。一方 for は、初期化・条件・更新をひとまとめに書けるようにした while の書きやすい形(糖衣構文)という位置づけで、判定タイミングは while と同じ「前」です。

// この for と…
for (let i = 0; i < 5; i++) {
  console.log(i);
}
 
// この while は、ほぼ同じことをしている
let i = 0;
while (i < 5) {
  console.log(i);
  i++;
}
js

つまり、for と while は「同じタイミングで判定するループの、書き方違い」です。do...while だけが判定タイミングそのものが違います。

判断は2段階で考える

ステップ1:ループに入る前に条件を判定できるか

判定できないなら do...while 系で確定です。「まず1回やってみないと、続けるかどうかが決められない」ケースがこれにあたります。

// 入力を受け取ってみないと、有効かどうか判定できない
let input;
do {
  input = prompt('1〜10の数字を入力してください');
} while (!isValid(input));
js

ステップ2:カウンタや反復対象があるか

判定できるなら、次は書き方の選択です。回数やインデックス、反復する対象があるなら for(配列などのイテラブルなら for...of)、そうではない継続条件なら while を使います。

// 回数が決まっている → for
for (let i = 0; i < retryCount; i++) { /* ... */ }
 
// 反復対象がある → for...of
for (const user of users) { /* ... */ }
 
// 「キューが空になるまで」など、カウンタではない継続条件 → while
while (queue.length > 0) {
  const task = queue.shift();
  run(task);
}
js
状況使うもの
事前に条件判定できない(必ず1回は実行したい)do...while / while (true) + break
回数・インデックスが決まっているfor
配列などイテラブルを順に処理するfor...of
配列の各要素に副作用を実行するforEach
カウンタではない継続条件があるwhile

do...while を実務であまり見かけない理由

JavaScriptの実務コードで do...while が登場することは、実はほとんどありません。

do...while の使いどころだった「必ず1回は実行する」パターンの多くは、while (true) と break の組み合わせで書かれることが多いためです。

// do...while で書いた場合
let input;
do {
  input = prompt('入力してください');
} while (!isValid(input));
 
// while (true) + break で書いた場合
while (true) {
  const input = prompt('入力してください');
  if (isValid(input)) break;
  if (cancelled) break; // 脱出条件を増やしやすい
}
js

while (true) + break が選ばれやすいのは、次の2点が理由です。

  1. 脱出条件が読みやすい位置にある — 判定が本体の中にあるので、「どこで抜けるのか」を上から読み下すだけで追えます
  2. 脱出条件を複数書ける — 「有効な入力が来たら」「キャンセルされたら」のように、抜ける理由が複数あるケースに素直に対応できます

逆に do...while の実務上の難点は、条件が末尾にあることです。本体が長くなると、条件までスクロールしないと「何のループなのか」が分かりません。


break と continue

break

ループや switch を途中で抜け出します。

const numbers = [1, 3, 5, 2, 8, 4];
 
for (const n of numbers) {
  if (n % 2 === 0) {
    console.log(`最初の偶数: ${n}`); // '最初の偶数: 2'
    break; // ループを終了
  }
}
js

continue

現在のイテレーションをスキップして、次のイテレーションに進みます。

for (let i = 0; i < 5; i++) {
  if (i % 2 === 0) continue; // 偶数はスキップ
  console.log(i); // 1, 3
}
js

return と throw — 制御フロー文

return と throw は、if や for と同様に**制御フロー文(Control Flow Statement)**です。どちらも、その後のコードを実行せずに処理の流れを変えます。

return — 関数を終了して値を返す

return は関数の実行をそこで終了し、呼び出し元に値を返します。

function divide(a, b) {
  if (b === 0) {
    return null; // ここで関数終了。以降は実行されない
  }
  return a / b;
}
 
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // null
js

return の後ろに書くもの(null や a / b)は**式(Expression)**です。return 自体は文なので、変数に代入したりできません。

// return は文なので、式としては使えない
const x = return 5; // SyntaxError
js

return だけ書いた場合、または return を省略した関数は undefined を返します。

function doSomething() {
  console.log('実行');
  return; // undefined を返す
}
 
function doNothing() {
  // return がない場合も undefined を返す
}
js

throw — 例外を投げて処理を中断する

throw は例外を発生させ、呼び出し元に向かってスタックを遡ります。try...catch でキャッチされるまで伝播し続けます。

function getUser(id) {
  if (!id) {
    throw new Error('id は必須です'); // ここで関数終了。例外を投げる
  }
  return fetchUser(id);
}
 
try {
  getUser(null);
} catch (error) {
  console.log(error.message); // 'id は必須です'
}
js

throw の後ろに書くもの(new Error(...))も式です。技術的には任意の値を投げられますが、Error オブジェクト(またはそのサブクラス)を使うのが慣習です。

throw new Error('メッセージ');          // 推奨
throw new TypeError('型が違います');    // 組み込みエラー型
throw new RangeError('範囲外です');     // 組み込みエラー型
throw 'エラー文字列';                   // 技術的には可能だが非推奨
throw 42;                               // 同上
js

return と throw の使い分け

return は正常系として値を返し、throw は異常系として呼び出し元に失敗を伝えます。
状況使うもの
正常な終了(値を返す)return
値を返さずに終了return(undefined が返る)
想定内の「早期終了」(ガード節)return
呼び出し元に問題を知らせたいthrow
引数が不正・前提条件を満たさないthrow
// 正常な早期終了 → return
function formatName(name) {
  if (!name) return '名前未設定'; // 値として返せるのでreturnでよい
  return name.trim();
}
 
// 呼び出し元に問題を知らせる → throw
function parsePositiveInt(str) {
  const num = parseInt(str, 10);
  if (isNaN(num)) {
    throw new TypeError(`'${str}' は数値に変換できません`);
  }
  if (num <= 0) {
    throw new RangeError(`正の整数が必要です: ${num}`);
  }
  return num;
}
js

まとめ

構文役割
if / else条件によって分岐
switch単一値の多分岐(break 必須)
for...ofイテラブルの反復
for...inオブジェクトのキー列挙(配列には使わない)
while条件が真の間繰り返す(判定は本体の前)
do...while先に1回実行してから判定(判定は本体の後)
breakループ・switch を抜ける
continue今のイテレーションをスキップ
return関数を終了して値を返す(制御フロー文)
throw例外を投げて処理を中断(制御フロー文)

throw を使った例外のキャッチ(try...catch)やカスタムエラークラスの詳細は、14章「エラーハンドリング」で扱います。

ステップアップしている男性のイラスト

次の章では、関数の定義方法と this の挙動を詳しく見ていきます。

参考リンク

JavaScriptクイズに挑戦するこの章で学んだJavaScriptの知識を、4択クイズでアウトプットして定着させよう