【入門編】forループ内でのlet宣言が生成する「ブロックスコープのクローン」とメモリ消費の最適化 – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

こんにちは!フロントエンドからNode.jsの内部挙動まで、日夜JavaScriptと向き合っているシニアエンジニアです。

JavaScriptを学び始めると、変数を宣言するキーワードとして `var` だけでなく、`let` や `const` が登場しますよね。「とりあえず再代入しないなら `const`、するなら `let` を使っておけばいいんでしょ?」と思っている方も多いのではないでしょうか。

実は、「`for`ループの中で `let` を使う」という何気ないコードの裏側では、V8エンジン(JavaScriptの実行エンジン)が私たちの想像を遥かに超えるスマートで高度なメモリ管理を行っています。ここをクリアすると、JavaScriptのスコープとメモリの挙動がスッと腑に落ちて、コードの解像度が一段と上がりますよ。

今日は、この「`for`ループ内での `let` 宣言が生成するブロックスコープのクローン」について、ガベージコレクション(GC)の挙動も交えながら、本質を優しく紐解いていきましょう!

—

1. 昔のトラウマ:`var` が引き起こす非同期処理のバグ

まずは、私たちがなぜ `let` の恩恵を知る必要があるのか、歴史的な背景(そして `var` の闇)を少しだけ振り返ってみましょう。

以下のコードを見てください。0から2までカウントアップしながら、1秒遅れてその数値をコンソールに出力したい、よくあるタイマー処理のコードです。

// 【危険なコード】varを使ったループと非同期処理
function oldSchoolLoop() {
for (var i = 0; i < 3; i++) { setTimeout(function() { console.log('現在のiの値:', i); }, 1000); } } oldSchoolLoop(); // 実行結果(1秒後): // 現在のiの値: 3 // 現在のiの値: 3 // 現在のiの値: 3 「あれ? `0, 1, 2` と出てほしいのに、なぜか `3` が3回出力されてしまったぞ……?」と頭を抱えた経験はありませんか?

なぜ `3` になってしまうのか?

`var` は「関数スコープ」を持つ変数です。つまり、`for`ループの外側から見ても、`i` はループブロックの外部(この場合は関数内全体、あるいはグローバル)にたった1つだけ存在する共有変数として扱われます。

頭の中のイメージはこんな感じです:

[メモリ空間]
変数 i = 「3」(最終的にループを抜けた時の値が、唯一の変数を上書きし続ける)
┗ 1秒後に実行される無名関数たち(すべてこの「共有された i」を参照している)

ループが高速で回し切られた時点ですでに `i` は `3` になっています。1秒後にタイマーのコールバック関数が実行されたときには、時すでに遅し。すべてのコールバックが、その時点ですでに `3` に書き換わっていた「ひとつの `i`」を参照してしまっていたわけですね。

—

2. 救世主 `let`:ループごとに生まれる「ブロックスコープのクローン」

では、ここで主役の `let` の登場です。先ほどの `var` を `let` に書き換えてみましょう。

// 【モダンなコード】letを使ったループと非同期処理
function modernLoop() {
for (let i = 0; i < 3; i++) { setTimeout(function() { console.log('現在のiの値:', i); }, 1000); } } modernLoop(); // 実行結果(1秒後): // 現在のiの値: 0 // 現在のiの値: 1 // 現在のiの値: 2 おぉ、見事に `0, 1, 2` と期待通りの結果になりましたね! 一体、内部で何が起きているのでしょうか?

ループの回数だけ「変数 `i` のクローン(別空間)」が生成されている

JavaScriptの仕様(ECMAScript)において、`for (let i = 0; …)` のように初期化部に `let` を使った場合、「ループの反復(イテレーション)が1回行われるごとに、新しいブロックスコープ(変数環境)が新しく作られる」という特殊なルールが適用されます。

イメージとしては、ループが回るたびに、V8エンジンが以下のような独立したカプセルを生成している感覚です。

【ループ1回目(i = 0 のとき)】
└─ [独立したスコープA] 内部の i = 0
└─ 1秒後のタイマーは、この「スコープAの i」をクロージャーとして保持!

【ループ2回目(i = 1 のとき)】
└─ [独立したスコープB] 内部の i = 1
└─ 1秒後のタイマーは、この「スコープBの i」をクロージャーとして保持!

【ループ3回目(i = 2 のとき)】
└─ [独立したスコープC] 内部の i = 2
└─ 1秒後のタイマーは、この「スコープCの i」をクロージャーとして保持!

このように、それぞれ別々のメモリ領域(スコープ)に独立した `i` が存在しているため、後から非同期処理が実行されても、自分が生まれた瞬間に割り当てられた正しい `i` の値を保持し続けられるのです。これが「ブロックスコープのクローン」の正体です。

—

3. ガベージコレクション(GC)とメモリ効率の最適化

「でも待って。ループの回数分だけ毎回スコープや変数が新しく作られるなら、メモリをすごく無駄遣いしている(メモリリークの原因になる)んじゃないの?」

鋭い方ならそう思われるかもしれません。さすがですね。
しかし、V8エンジンのようなモダンなJavaScriptランタイムは、私たちが思っている以上に賢くできています。

使われなくなったスコープは綺麗に回収される

`let` によって生成された個別のブロックスコープは、そのスコープに紐づく処理(今回の例でいう `setTimeout` のコールバックなど)がすべて実行され終わると、ガベージコレクション(GC)の回収対象になります。

1. ループが回ってスコープが作られる。
2. その中で作られた非同期処理が参照を保持する。
3. 非同期処理の実行が完了すると、そのスコープを参照しているものがどこにもなくなる。
4. V8のGCが「もう誰も使ってないな」と判断し、メモリ空間から安全に解放する。

もし非同期処理がなく、単にループ内で同期的に処理を完結させる場合であれば、V8の最適化コンパイラは不要なメモリ確保を極限まで省くインライン展開などを行います。つまり、開発者は安全でバグのないコード(`let`によるスコープの分離)を書き、メモリの細かい管理や最適化はV8エンジンに安心してお任せできるというエコシステムになっているのです。

—

4. 陥りやすい罠:`const` をループのカウンターに使えない理由

ここで、もう一歩踏み込んでみましょう。「じゃあ、再代入しないんだから `const` のほうがもっと安全なんじゃないの?」と思って、以下のように書いたことはありませんか?

// 【エラーになるコード】
for (const i = 0; i < 3; i++) { // SyntaxError! console.log(i); } これは実行時に構文エラー(SyntaxError)になります。 なぜなら、`for (let i = 0; ...)` のループの仕組みは、「1回ループが回るごとに、新しい値で変数 `i` を再宣言(再初期化)している」からです。

ループの2回目を回るとき、JavaScriptは内部的に以下のような処理を模倣しています。

// ループの2回目にやろうとしていること(イメージ)
const i = 0; // 1回目
// … 処理が終わった後、次のループへ …
const i = 1; // 2回目(「constなのに再宣言・再代入しようとしている!」と怒られる)

`const` は一度宣言したら値を変えられない(再代入不可)定数ですから、ループのカウンターのように「回数を重ねるごとに値がインクリメント(更新)される変数」としては使えないというわけですね。

  • ループのカウンターのように値が変化するものには `let`
  • ループのなかで一度代入したら二度と書き換えない定数には `const`(※これはループブロック内で毎回新しく定数として定義する場合に使えます)

この使い分けを体に染み込ませておきましょう。

—

まとめ:モダンJSの恩恵をフルに受けるために

いかがだったでしょうか? 今回の重要なポイントをサクッとまとめておきます。

1. `var` は関数スコープなので、ループ内で変数が共有されてしまい非同期処理でバグる。
2. `let` を `for` の初期化部に使うと、ループの回数ごとに「ブロックスコープのクローン」が生成されるため、値が安全に保たれる。
3. 不要になったスコープはV8エンジンのガベージコレクション(GC)が綺麗に回収してくれるため、メモリ効率の心配もいらない。
4. ループのカウンター自体は値が変化するため、`const` ではなく `let` を使う必要がある。

ここをしっかりと理解しておけば、クロージャーや非同期処理が絡む複雑なアルゴリズムを実装する際も、予期せぬバグに悩まされることがなくなります。

JavaScriptのランタイムやメモリの動きを少し意識するだけで、書くコードの質は見違えるほど洗練されます。この調子で、一緒に最高峰のモダンJavaScriptマスターを目指していきましょう!

タイトルとURLをコピーしました