ステップ実行(変数の状態を逐次表示)で理解がぐっと深まるタイプの解説を作ります。
こちらは「関数」問題群から、学習効果が高い代表的な5問を選んで、変数・スタック・出力を追いながら丁寧にステップ実行で解説します。
(もし「この中の1問だけ詳しく」「別の問題も見たい」など希望があれば、そのまま言ってください — ただし確認を待たずに進めるよう指定がありましたので、まず代表5問でまとめます。)
対象(5問)
- クロージャ(makeCounter)
- 可変長引数と reduce(sum(…nums))
- 高階関数(makeAdder)
- メモ化による再帰(memoFib) — 詳細は fib(6) をステップで追って要点は fib(10)=55 を示します
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ステップ実行(行番号は説明用)
function makeCounter() { ... }を評価して関数オブジェクトを作る(関数定義)。グローバルにmakeCounterが存在。- グローバル環境:
makeCounter→
- グローバル環境:
const c = makeCounter();を実行 → makeCounter が呼ばれる- 呼び出しスタック:
makeCounter()実行中 - makeCounter の内部(実行コンテキスト):
- ローカル変数:
count = 0
- ローカル変数:
- makeCounter の
return function() { ... }が評価され、**無名関数(クロージャ)**が作られる。重要:この関数はcountへ参照を持つ(環境をキャプチャ)。 - makeCounter の実行が終わり、戻り値(クロージャ関数)が
cに代入される。 - スタック: (戻る) → グローバルへ
- グローバル状態:
c→ <Function (クロージャ), 内部参照: count(現在 0)>
- 呼び出しスタック:
c();を実行(1回目)- 呼び出しスタック: 実行中
c()(無名関数) - この関数はキャプチャした環境にアクセスできる →
countは 0 - 実行:
count++→countbecomes1 console.log(count)→ 出力1- 戻る。環境にある
countは1のまま保持される。
- 呼び出しスタック: 実行中
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ステップ実行
sum定義。...numsは「渡された引数を配列にする」ことを意味。sum(1,2,3,4)呼び出し- 実行コンテキスト内:
nums = [1,2,3,4]
- 実行コンテキスト内:
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
- 初期値
- return 10 →
console.logが10を出力
最終出力
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ステップ実行
makeAdder定義。グローバルに登録。const add5 = makeAdder(5);makeAdder呼び出し中: ローカルx = 5- 返されるのは
function(y){ return x + y; }:この関数はxをキャプチャ(5) add5に代入:add5は「x=5 を覚えた関数」
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 = {}
呼び出しチェーン(深さとキャッシュの変化に注目)
fib(6)呼び出しn=6,n>1,cache[6]未定義 → 計算fib(5) + fib(4)
fib(5)呼び出しn=5→ 計算fib(4) + fib(3)
fib(4)呼び出しn=4→ 計算fib(3) + fib(2)
fib(3)呼び出しn=3→ 計算fib(2) + fib(1)
fib(2)呼び出しn=2→ 計算fib(1) + fib(0)
fib(1)→return 1fib(0)→return 0- 戻って
fib(2)は1 + 0 = 1 cache[2] = 1(保存)- 現在
cache = {2:1}
- 戻って
- 戻って
fib(3)はfib(2) + fib(1)→1 + 1 = 2cache[3] = 2cache = {2:1, 3:2}
- 戻って
fib(4)はfib(3) + fib(2)→2 + 1 = 3cache[4] = 3cache = {2:1,3:2,4:3}
- 戻って
fib(5)needsfib(4) + fib(3):fib(4)→ cache にある → 3 を即返す(ここで再計算はしない)fib(3)→ cache にある → 2 を即返すfib(5) = 3 + 2 = 5cache[5] = 5cache = {2:1,3:2,4:3,5:5}
fib(4)(the other branch for fib(6)) is in cache → 3fib(6) = fib(5) + fib(4) = 5 + 3 = 8cache[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
ステップ実行と状態
- オブジェクト定義 →
objにnameとgreet(メソッド)がある。obj = { name: "Z", greet: <Function> }
const f = obj.greet;fは関数参照をコピーしただけ。この時点ではfに obj の情報(this)は束縛されない。f→ same function object asobj.greet.
f();の呼び出し(A)- 重要:呼び出し形態が「単独関数呼び出し」なので
thisは 非メソッド呼び出し として扱われる。 - strict モードであれば
thisはundefined、非 strict だとグローバルオブジェクト(ブラウザならwindow)になる。 console.log(this.name)→thisが期待のobjではないためundefined(strict)やwindow.name(非 strict)を参照 → 通常undefinedが出る。- 出力例(厳格モード):
undefined
- 重要:呼び出し形態が「単独関数呼び出し」なので
const g = obj.greet.bind(obj);bind(obj)は新しい関数を返し、その内部でthisを常にobjに固定する。gはobjに束縛された関数。
g();の呼び出し(B)- 呼ばれると
thisは常にobj→console.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。

