制御構文 — if・switch・for・whileの実務パターン
この章の目次開く
- 条件分岐:if / else
- 早期 return パターン
- 条件分岐:switch
- フォールスルーに注意
- オブジェクトマッピングで switch を置き換える
- ループ:for / for...of / for...in
- for 文
- for...of
- for...in
- while
- do...while
- for・while・do...while の使い分け
- 判断は2段階で考える
- do...while を実務であまり見かけない理由
- break と continue
- break
- continue
- return と throw — 制御フロー文
- return — 関数を終了して値を返す
- throw — 例外を投げて処理を中断する
- return と throw の使い分け
- まとめ
- 参考リンク
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('可');
}早期 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);
}条件分岐:switch
複数の値と比較するときは switch が読みやすくなります。
const day = 'Monday';
switch (day) {
case 'Saturday':
case 'Sunday':
console.log('週末');
break;
case 'Monday':
console.log('月曜日'); // こちらが実行される
break;
default:
console.log('平日');
}フォールスルーに注意
break を書き忘れると、次の case の処理も続けて実行されます(フォールスルー)。
switch (status) {
case 'pending':
console.log('pending');
// break がないと次の case も実行される!
case 'active':
console.log('active'); // status が 'pending' のときも実行される
break;
}意図的なフォールスルー(複数の値に同じ処理)以外は、必ず 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] ?? '不明';
}マッピングをオブジェクトとして定義すると、データとロジックが分離されてテストしやすくなります。ただし、条件に複雑なロジックが含まれる場合(範囲チェック、複数条件の組み合わせなど)は if/else や switch の方が読みやすいです。
ループ:for / for...of / for...in
for 文
インデックスが必要な場合や、回数を指定して繰り返すときに使います。
for (let i = 0; i < 5; i++) {
console.log(i); // 0, 1, 2, 3, 4
}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', ...
}[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']
// ]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);
}左辺の [index, item] は配列を作っているのではなく、右辺の配列を先頭から順に取り出して名前を付ける記法です。1周目には [0, 'a'] が渡ってくるので、index に 0、item に 'a' が入ります。

つまり 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
}while
条件が満たされる間、繰り返し実行します。ループに入る前に条件を判定するため、最初から条件を満たしていなければ本体は一度も実行されません。
let count = 0;
while (count < 3) {
console.log(count); // 0, 1, 2
count++;
}do...while
本体を先に1回実行してから条件を判定します。そのため、条件を満たしていなくても最低1回は実行されます。
let n = 10;
do {
console.log(n); // 10(条件を満たさなくても1回実行される)
n++;
} while (n < 5);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++;
}つまり、for と while は「同じタイミングで判定するループの、書き方違い」です。do...while だけが判定タイミングそのものが違います。
判断は2段階で考える
ステップ1:ループに入る前に条件を判定できるか
判定できないなら do...while 系で確定です。「まず1回やってみないと、続けるかどうかが決められない」ケースがこれにあたります。
// 入力を受け取ってみないと、有効かどうか判定できない
let input;
do {
input = prompt('1〜10の数字を入力してください');
} while (!isValid(input));ステップ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);
}| 状況 | 使うもの |
|---|---|
| 事前に条件判定できない(必ず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; // 脱出条件を増やしやすい
}while (true) + break が選ばれやすいのは、次の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; // ループを終了
}
}continue
現在のイテレーションをスキップして、次のイテレーションに進みます。
for (let i = 0; i < 5; i++) {
if (i % 2 === 0) continue; // 偶数はスキップ
console.log(i); // 1, 3
}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)); // nullreturn の後ろに書くもの(null や a / b)は**式(Expression)**です。return 自体は文なので、変数に代入したりできません。
// return は文なので、式としては使えない
const x = return 5; // SyntaxErrorreturn だけ書いた場合、または return を省略した関数は undefined を返します。
function doSomething() {
console.log('実行');
return; // undefined を返す
}
function doNothing() {
// return がない場合も undefined を返す
}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 は必須です'
}throw の後ろに書くもの(new Error(...))も式です。技術的には任意の値を投げられますが、Error オブジェクト(またはそのサブクラス)を使うのが慣習です。
throw new Error('メッセージ'); // 推奨
throw new TypeError('型が違います'); // 組み込みエラー型
throw new RangeError('範囲外です'); // 組み込みエラー型
throw 'エラー文字列'; // 技術的には可能だが非推奨
throw 42; // 同上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;
}まとめ
| 構文 | 役割 |
|---|---|
if / else | 条件によって分岐 |
switch | 単一値の多分岐(break 必須) |
for...of | イテラブルの反復 |
for...in | オブジェクトのキー列挙(配列には使わない) |
while | 条件が真の間繰り返す(判定は本体の前) |
do...while | 先に1回実行してから判定(判定は本体の後) |
break | ループ・switch を抜ける |
continue | 今のイテレーションをスキップ |
return | 関数を終了して値を返す(制御フロー文) |
throw | 例外を投げて処理を中断(制御フロー文) |
throw を使った例外のキャッチ(try...catch)やカスタムエラークラスの詳細は、14章「エラーハンドリング」で扱います。

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