JavaScriptの再帰関数を初心者向けにやさしく解説
まず、不安や「難しそう…」って気持ちがあるならそれで普通。再帰は最初ひっかかりやすいテーマだけど、ポイントさえ掴めば一気に分かりやすくなる。例題で「どう動いているのか」を目で追えるようにしていくね。
再帰の基本と考え方
- 定義: 関数の中で「自分自身」をもう一度呼び出す書き方のこと。
- イメージ: 問題を「少しだけ小さい同じ問題」に分けて、終わりまで小さくしていく。
- 絶対ルール: 終了条件(ベースケース)を必ず作る。これがないと永遠に呼び出しが続いて止まらない。
たとえるなら、階段を下りるときに「1段ずつ下りる(問題を小さくする)」と「1階に着いたら終わり(終了条件)」の2つが必要、という感じ。
再帰で大事な2パーツ
- 終了条件(ベースケース): ここに来たらもう呼び出しをやめて値を返す。
- 再帰ステップ: 問題を少し小さくして、同じ関数をもう一度呼ぶ。
function example(n) {
// 終了条件
if (n === 0) return;
// ここに処理を書く(例: 表示)
console.log(n);
// 再帰ステップ(問題を小さく)
example(n - 1);
}
JavaScript例題1:カウントダウン(流れが見える入門)
ループでの書き方
for (let n = 5; n > 0; n--) {
console.log(n);
}
JavaScript再帰での書き方(同じ動き)
function countdown(n) {
if (n === 0) {
console.log("Start!");
return; // 終了条件
}
console.log(n);
countdown(n - 1); // 問題を少し小さく
}
countdown(5);
// 5
// 4
// 3
// 2
// 1
// Start!
JavaScript- ポイント: 呼ぶたびに数字が1つずつ小さくなる。0になったら「Start!」を出して終了。
例題2:階乗(「戻りながら計算する」タイプ)
- 意味: 階乗は「(n!) は (n \times (n-1)!)」で定義される。特別に「(0! = 1)」がベースケース。
- 再帰の動き: 下へ下へ潜って、最後に戻りながら掛け算が完成する。
function factorial(n) {
if (n === 0) return 1; // 0! = 1(終了条件)
return n * factorial(n - 1); // 再帰ステップ
}
console.log(factorial(5)); // 120
JavaScript- 頭の中の動き:
- factorial(5) = 5 × factorial(4)
- factorial(4) = 4 × factorial(3)
- …
- factorial(0) = 1(ここで止まる)
- 1をもって順に「戻りながら」掛け算が解決される。
例題3:配列の合計(ループが苦手な人に優しい考え方)
ループでの書き方
const arr = [2, 4, 6];
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
console.log(sum); // 12
JavaScript再帰での書き方
function sumArray(arr, i = 0) {
if (i === arr.length) return 0; // 終了条件: 末尾まで来たら0
return arr[i] + sumArray(arr, i + 1); // 現在の要素 + 残りの合計
}
console.log(sumArray([2, 4, 6])); // 12
JavaScript- ポイント: 「先頭の値 + 残り全部の合計」という形に分解していく。
例題4:入れ子のデータを全部なめる(再帰が光る場面)
入れ子(ネスト)構造は、深さが分からないからループだけだと大変。再帰だと「中身が配列ならもう一度同じことをする」と書ける。
function flatten(sum, data) {
for (const item of data) {
if (Array.isArray(item)) {
sum = flatten(sum, item); // 配列ならさらに深く
} else {
sum += item; // 値なら加算(例の処理)
}
}
return sum;
}
const nested = [1, [2, [3, 4]], 5];
console.log(flatten(0, nested)); // 15
JavaScript- ポイント: 形が同じなら「同じ関数」をまた使える。それが再帰の強み。
再帰 vs ループの違い(どっちを選ぶ?)
- 分かりやすさ:
- ループ: 処理の順番が一直線で見えやすい。
- 再帰: 入れ子構造や「同じ形の問題に分割」する時はシンプルに書ける。
- 性能と注意:
- 再帰: 呼び出しが深すぎるとメモリ(スタック)を使いすぎることがある。
- ループ: 一般に軽くて安全。深さの制限がない。
- 結論: 木構造やネストの走査、分割統治(クイックソートなど)は再帰が得意。それ以外はループで十分なことが多い。
よくあるつまずきと回避法
- 終了条件が曖昧: 明確に「ここで止める」を書く。数値なら「0になったら」、配列なら「末尾まで来たら」など。
- 問題を小さくし忘れ: nを減らす、インデックスを進める、配列の残りを渡すなど、1歩進める処理を必ず入れる。
- 副作用だらけ: なるべく「引数を変えて同じ関数を呼ぶ」形に。グローバル変数の書き換えは混乱のもと。
手を動かす練習問題
- 練習1:
countUp(n)を作って、1からnまで表示する(終了条件と再帰ステップを自分で決める)。 - 練習2:
sumNested(arr)を作って、入れ子配列の数値合計を返す。 - 練習3: 文字列を反転する
reverse(str)を再帰で書く(先頭 + 残り、の分割を考える)。 - 練習4:
maxNested(arr)を作って、入れ子配列から最大値を見つける。
ヒントは「終了条件を先に書く → 問題を1歩だけ小さくして同じ関数を呼ぶ」。この順番で組み立てると迷いにくい。
模範解答と解説
練習1:countUp(n)
問題: 1からnまで順番に表示する。
解答例
function countUp(n, current = 1) {
if (current > n) return; // 終了条件
console.log(current); // 表示
countUp(n, current + 1); // 次の数へ
}
countUp(5);
// 1
// 2
// 3
// 4
// 5
JavaScript解説
currentを1から始めて、毎回 +1 していく。current > nになったら終了。- 「終了条件」と「1歩進める」がそろっているので無限ループにならない。
練習2:sumNested(arr)
問題: 入れ子(ネスト)した配列の合計を求める。
解答例
function sumNested(arr) {
let sum = 0;
for (const item of arr) {
if (Array.isArray(item)) {
sum += sumNested(item); // 配列なら再帰
} else {
sum += item; // 数値なら加算
}
}
return sum;
}
console.log(sumNested([1, [2, [3, 4]], 5])); // 15
JavaScript解説
- 配列の中にさらに配列があったら「同じ処理をもう一度」呼ぶ。
- これが再帰の真骨頂。深さがいくらあっても同じ関数で処理できる。
練習3:reverse(str)
問題: 文字列を逆順にする。
解答例
function reverse(str) {
if (str === "") return ""; // 終了条件: 空文字
return reverse(str.slice(1)) + str[0];
}
console.log(reverse("hello")); // "olleh"
JavaScript解説
str.slice(1)で先頭を除いた残りを再帰的に反転。- 最後に
str[0]を後ろにくっつける。 - こうして戻りながら文字が逆順に並ぶ。
練習4:maxNested(arr)
問題: 入れ子配列の中から最大値を探す。
解答例
function maxNested(arr) {
let max = -Infinity;
for (const item of arr) {
if (Array.isArray(item)) {
max = Math.max(max, maxNested(item)); // 配列なら再帰
} else {
max = Math.max(max, item); // 数値なら比較
}
}
return max;
}
console.log(maxNested([1, [7, [3, 9]], 5])); // 9
JavaScript解説
- 合計のときと同じ構造だが、処理が「足し算」ではなく「最大値の比較」になっている。
- 再帰のパターンは「配列なら再帰」「値なら処理」という形で応用できる。
まとめ
- countUp → 数字を1歩ずつ進める
- sumNested → ネストをほどきながら合計
- reverse → 先頭と残りに分けて逆順に組み立て
- maxNested → ネストをほどきながら最大値を更新
再帰のコツは「終了条件」と「問題を小さくする」の2つを必ずセットにすること。
