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

こんにちは!フロントエンドからNode.jsの深層まで、日夜JavaScriptと向き合っているエンジニアのあなたへ。今日は、JavaScriptの「変数とスコープ」に関する、とてもエキサイティングで重要な核心の話をしますね。

「ループの中で `let` を使うと、なぜか変数がイテレーション(反復)ごとに独立して生き残る」――この現象、不思議に思ったことはありませんか?
「古い `var` の世界ならおかしな挙動になっていたのに、なぜ `let` だとうまくいくのか?」

ここをクリアすれば、JavaScriptのスコープチェーンやクロージャの挙動はバッチリマスターできますよ。V8エンジンの裏側の息づかいまで感じられるような、本質的な世界へ一緒に踏み込んでいきましょう!

—

1. タイムトラベルの終焉:`var` が抱えていた「ひとつの変数」の呪縛

まずは、歴史を少しだけ振り返ってみましょう。モダンなJavaScriptになる前、私たちの世界には `var` しかありませんでした。

次のコードを見てください。あなたなら、コンソールに何が表示されると予想しますか?

// 【歴史的遺物】varを使ったループの例
for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(`[var] 現在の i の値: ${i}`); }, 100); } 直感的には、「0、1、2」と順番に出力されてほしいですよね。でも、実際に実行してみると……驚くなかれ、「3、3、3」と表示されてしまいます。

なぜこんなことが起きるのでしょうか?
それは、`var` のスコープが「関数スコープ(またはグローバルスコープ)」だからです。`for` ループの中で宣言された `i` は、ループの外側(あるいは同じ関数内)から見ても「たったひとつの同じ変数」を指し続けています。

V8エンジンのメモリ空間のイメージを覗いてみましょう。

[メモリ空間:var の場合]
変数 i ──> [ 値: 3 ] (※ループが終わった瞬間に最後の値「3」に固定される)
↑
setTimeoutのコールバックたちが
「みんなで同じ i を見つめている」状態

`setTimeout` のタイマーが実際に発火する頃には、すでに `for` ループは高速で回りきってしまい、変数 `i` の値は `3` に書き換わってしまっています。その結果、すべての非同期処理が「3」を参照してしまうという、JavaScript初学者が必ず一度はハマる罠が生まれていたのです。

—

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

ここで、今回の主役である `let` の登場です。
先ほどのコードの `var` を `let` に書き換えてみましょう。

// 【モダンJS】letを使ったループの例
for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(`[let] 現在の i の値: ${i}`); }, 100); } これを実行すると、期待通り 「0」「1」「2」 が順番に出力されます。素晴らしいですね!

「えっ、一体何が起きているの?」と思いますよね。実はこれこそが、ECMAScript仕様(ES2015)がJavaScriptにもたらした、最も美しい魔法の一つです。

`for (let i = 0; i …)` のように、初期化式で `let` を使用した場合、JavaScriptエンジンは「各反復(イテレーション)のブロック内部専用の新しい変数(環境レコード)」を毎回合法的かつ自動的に生成(クローン)します。

V8エンジンの内部、ヒープメモリ上では次のようなドラマが繰り広げられています。

[メモリ空間:let の場合]
反復 00回目: ──> [ ブロックスコープ A ] ──> 変数 i (値: 0) <── [クロージャ A が保持] 反復 01回目: ──> [ ブロックスコープ B ] ──> 変数 i (値: 1) <── [クロージャ B が保持] 反復 02回目: ──> [ ブロックスコープ C ] ──> 変数 i (値: 2) <── [クロージャ C が保持] つまり、ループが回るたびに、前回の `i` とは完全に別個のメモリ領域に新しい `i` が生み出され、その瞬間の値がコピー(束縛)されるのです。
`setTimeout` の中に作られた無名関数(クロージャ)は、その生成された瞬間に存在していた「その回専用の `i`」をしっかりと掴んで離さないため、時間が経って実行されても「0、1、2」を保持し続けることができるというわけですね。

—

3. 陥りやすい罠:`const` でループを回そうとしたときの悲劇

ここで一つ、実務の現場で開発者がやりがちな「うっかりミス」についても触れておきましょう。

「ループ変数が書き換わらないなら、より安全な `const` でループを回せば最強なのでは?」と考えたことはありませんか?

// 【注意】constでループを回そうとすると…?
for (const i = 0; i < 3; i++) { console.log(i); } これを実行すると、JavaScriptエンジン(V8など)は即座に次のようなエラー(SyntaxError)を吐き出して怒り出します。 > `TypeError: Assignment to constant variable.`
> (定数への代入はできません)

なぜエラーになるのでしょうか?
それは、`i++` という記述が文字通り「変数 `i` に新しい値を再代入(Assignment)している」からです。

`const` は「再代入を一切許さない」という強い制約を持つ変数宣言です。したがって、ループの条件式やインクリメント部分で `i` の値を更新しようとする `for (const i = …; …; i++)` は、文法的に矛盾してしまうためエラーになります。

じゃあ、オブジェクトや配列のプロパティなら `const` でいける?

ちなみに、ループの中でそのイテレーション固有の「定数」として扱いたい値がある場合は、次のようにブロック内部で `const` を宣言するのがモダンなアプローチです。

const items = [‘apple’, ‘banana’, ‘orange’];

// ループ自体のカウンターは let、ループ内で使う不変の値は const にする
for (let i = 0; i < items.length; i++) { const currentItem = items[i]; // このブロック内だけで生きる定数 setTimeout(() => {
console.log(`インデックス ${i}: ${currentItem}`);
}, 100);
}

この書き方であれば、`i` はループエンジンによって安全に管理され、`currentItem` はそのブロックの中だけの安全なリードオンリーの定数としてクロージャにキャプチャされます。非常に美しく、堅牢なコードですね。

—

まとめ:言語の背後にある「メモリとスコープの仕組み」を愛そう

いかがでしたでしょうか?
ただ「`var` じゃなくて `let` を使えば動く」と表面的に覚えるだけよりも、

  • `var` は関数全体でひとつの変数を共有してしまう
  • `let` は `for` ループの各反復ごとに独立した変数のクローン(スコープ)を生成する
  • この仕組みのおかげで、非同期処理やクロージャが「期待通りの値」を安全にキャプチャできる

というランタイムの挙動までイメージできるようになると、コードを書くときの視座が劇的に変わります。

JavaScriptという言語は、一見するとお気楽に動くように見えて、その下層では非常に洗練されたスコープチェーンのメカニズムが働いています。ここをクリアしたあなたなら、もう変数のスコープやクロージャで迷うことは怖くないはずです。

明日からのコーディングで、ぜひこの「ブロックスコープのクローン」の躍動を脳内でトレースしてみてくださいね。ハッピー・コーディング!

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