JavaScript | レベル別練習問題:関数

JavaScript
スポンサーリンク

ステップ実行(変数の状態を逐次表示)で理解がぐっと深まるタイプの解説を作ります。
こちらは「関数」問題群から、学習効果が高い代表的な5問を選んで、変数・スタック・出力を追いながら丁寧にステップ実行で解説します。
(もし「この中の1問だけ詳しく」「別の問題も見たい」など希望があれば、そのまま言ってください — ただし確認を待たずに進めるよう指定がありましたので、まず代表5問でまとめます。)

対象(5問)

  1. クロージャ(makeCounter)
  2. 可変長引数と reduce(sum(…nums))
  3. 高階関数(makeAdder)
  4. メモ化による再帰(memoFib) — 詳細は fib(6) をステップで追って要点は fib(10)=55 を示します
  5. this の切れ・bind(obj.greet を変数に代入して呼ぶケース)

各問題とも、(A)コード、(B)ステップ実行(行ごとに変数とスタックの状態)、(C)最終出力・ポイント、の順で示します。


1 — クロージャ:makeCounter

function makeCounter() {
  let count = 0;
  return function() {
    count++;
    console.log(count);
  }
}
const c = makeCounter();
c();
c();
JavaScript

ステップ実行(行番号は説明用)

  1. function makeCounter() { ... } を評価して関数オブジェクトを作る(関数定義)。グローバルに makeCounter が存在。
    • グローバル環境: makeCounter
  2. const c = makeCounter(); を実行 → makeCounter が呼ばれる
    • 呼び出しスタック: makeCounter() 実行中
    • makeCounter の内部(実行コンテキスト):
      • ローカル変数: count = 0
    • makeCounter の return function() { ... } が評価され、**無名関数(クロージャ)**が作られる。重要:この関数は count へ参照を持つ(環境をキャプチャ)。
    • makeCounter の実行が終わり、戻り値(クロージャ関数)が c に代入される。
    • スタック: (戻る) → グローバルへ
    • グローバル状態: c → <Function (クロージャ), 内部参照: count(現在 0)>
  3. c(); を実行(1回目)
    • 呼び出しスタック: 実行中 c()(無名関数)
    • この関数はキャプチャした環境にアクセスできる → count は 0
    • 実行: count++count becomes 1
    • console.log(count) → 出力 1
    • 戻る。環境にある count1 のまま保持される。
  4. c(); を実行(2回目)
    • 同様に呼ばれ、現在の count = 1 → count++ -> 2
    • console.log(2) → 出力 2

最終出力

1
2

ポイント

  • クロージャは関数が生成されたときの外側スコープ(ここでは count)を参照し続ける。makeCounter の実行は終わっても count は消えない(ガーベジコレクションは参照が無くなったら回収する点に注意)。

2 — 可変長引数と reduce:sum(...nums)

function sum(...nums) {
  return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1,2,3,4)); // => 10
JavaScript

ステップ実行

  1. sum 定義。...nums は「渡された引数を配列にする」ことを意味。
  2. sum(1,2,3,4) 呼び出し
    • 実行コンテキスト内: nums = [1,2,3,4]
  3. nums.reduce((a,b)=>a+b, 0) の流れ(reduce の逐次処理)
    • 初期値 acc = 0
    • イテレーション1: cur=1 → acc = 0 + 1 = 1
    • イテレーション2: cur=2 → acc = 1 + 2 = 3
    • イテレーション3: cur=3 → acc = 3 + 3 = 6
    • イテレーション4: cur=4 → acc = 6 + 4 = 10
  4. return 10 → console.log10 を出力

最終出力

10

ポイント

  • ...(rest)でまとめた nums は普通の配列なので .reduce 等の配列メソッドが使える。
  • reduce の初期値を忘れると空配列でエラーになったり、期待と違う結果になることがある(初期値は安全)。

3 — 高階関数:makeAdder

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}
const add5 = makeAdder(5);
console.log(add5(10)); // => 15
JavaScript

ステップ実行

  1. makeAdder 定義。グローバルに登録。
  2. const add5 = makeAdder(5);
    • makeAdder 呼び出し中: ローカル x = 5
    • 返されるのは function(y){ return x + y; }:この関数は x をキャプチャ(5)
    • add5 に代入: add5 は「x=5 を覚えた関数」
  3. add5(10) 呼び出し
    • 実行時にクロージャ内部から x を参照(5)→ return 5 + 10 = 15

出力

15

ポイント

  • 高階関数は「関数を返す」「関数を引数に取る」関数のこと。ここでは「部分適用/状態保持」の簡単な例。
  • クロージャを使うことで、パラメータを固定した新しい関数を生成できる。

4 — メモ化された再帰:memoFib(詳細は fib(6) を追う)

元コード(簡略):

function memoFib() {
  const cache = {};
  return function fib(n) {
    if (n <= 1) return n;
    if (cache[n]) return cache[n];
    cache[n] = fib(n-1) + fib(n-2);
    return cache[n];
  }
}
const fib = memoFib();
console.log(fib(6)); // we'll trace this (fib(10)=55 is final note)
JavaScript

注:cache[n] の判定を if (cache[n]) と書くと 0 を falsy と扱って誤動作する可能性があります(cache[0] === 0)。安全には if (n in cache)if (cache.hasOwnProperty(n)) を使うのが本番では推奨です。ここでは理解しやすさのため元の形をそのまま使います(小さい n では支障なし)。

目的

再帰的フィボナッチは同じ部分問題を何度も計算する → メモ化で一度計算した値を保存して再利用(高速化)。

ステップ実行(fib(6) を例に)

初期: cache = {}

呼び出しチェーン(深さとキャッシュの変化に注目)

  1. fib(6) 呼び出し
    • n=6, n>1, cache[6] 未定義 → 計算 fib(5) + fib(4)
  2. fib(5) 呼び出し
    • n=5 → 計算 fib(4) + fib(3)
  3. fib(4) 呼び出し
    • n=4 → 計算 fib(3) + fib(2)
  4. fib(3) 呼び出し
    • n=3 → 計算 fib(2) + fib(1)
  5. fib(2) 呼び出し
    • n=2 → 計算 fib(1) + fib(0)
  6. fib(1)return 1
  7. fib(0)return 0
    • 戻って fib(2)1 + 0 = 1
    • cache[2] = 1(保存)
    • 現在 cache = {2:1}
  8. 戻って fib(3)fib(2) + fib(1)1 + 1 = 2
    • cache[3] = 2
    • cache = {2:1, 3:2}
  9. 戻って fib(4)fib(3) + fib(2)2 + 1 = 3
    • cache[4] = 3
    • cache = {2:1,3:2,4:3}
  10. 戻って fib(5) needs fib(4) + fib(3):
    • fib(4) → cache にある → 3 を即返す(ここで再計算はしない)
    • fib(3) → cache にある → 2 を即返す
    • fib(5) = 3 + 2 = 5
    • cache[5] = 5
    • cache = {2:1,3:2,4:3,5:5}
  11. fib(4) (the other branch for fib(6)) is in cache → 3
  12. fib(6) = fib(5) + fib(4) = 5 + 3 = 8
    • cache[6] = 8
    • 最終戻値 8

出力(fib(6))

8

補足

  • fib(10) を同様に計算すると 55(元の問題での出力)。メモ化がない純再帰だと指数時間になるが、メモ化でほぼ線形に近い時間で求まる。
  • 実装の安全性:if (cache[n])cache[0] = 0 を誤って無視する可能性あり。実務では if (n in cache) を使うか初期化・判定に注意する。

5 — this の切れと bind

const obj = {
  name: "Z",
  greet() { console.log(this.name); }
};
const f = obj.greet;
f();               // A
const g = obj.greet.bind(obj);
g();               // B

ステップ実行と状態

  1. オブジェクト定義 → objnamegreet(メソッド)がある。
    • obj = { name: "Z", greet: <Function> }
  2. const f = obj.greet;
    • f は関数参照をコピーしただけ。この時点では f に obj の情報(this)は束縛されない
    • f → same function object as obj.greet.
  3. f(); の呼び出し(A)
    • 重要:呼び出し形態が「単独関数呼び出し」なので this非メソッド呼び出し として扱われる。
    • strict モードであれば thisundefined、非 strict だとグローバルオブジェクト(ブラウザなら window)になる。
    • console.log(this.name)this が期待の obj ではないため undefined(strict)や window.name(非 strict)を参照 → 通常 undefined が出る。
    • 出力例(厳格モード): undefined
  4. const g = obj.greet.bind(obj);
    • bind(obj) は新しい関数を返し、その内部で this を常に obj に固定する。
    • gobj に束縛された関数。
  5. g(); の呼び出し(B)
    • 呼ばれると this は常に objconsole.log(this.name)"Z" を出力。

出力(まとめ)

A: undefined(環境依存だが一般に期待どおりの Z とはならない)
B: Z

ポイント

  • メソッド参照を変数に代入すると this が失われる(メソッド呼び出しコンテキストが消える)。
  • 修正方法:
    • obj.greet() のように直接呼ぶ(最もシンプル)。
    • obj.greet.bind(obj) で常に this を固定する。
    • アロー関数は this をレキシカルに捕まえるので、場合によってはアロー関数でラップする(ただしプロトタイプメソッドに使うとインスタンス共有の利点が減るので注意)。

まとめメモ(学習のコツ)

  • クロージャ:外側の変数が生き続ける。状態を隠蔽できる(カプセル化)。
  • 可変長引数:... で配列化 → map/reduce が使える。
  • 高階関数:状態固定(部分適用)や関数合成に有用。
  • メモ化:重複計算を避ける。再帰の性能が劇的に改善する。
  • this:呼び出し形態に依存する。bind/call/apply で制御できる。アロー関数はレキシカル this
タイトルとURLをコピーしました