JavaScriptの「var」と「let」:forループでハマる「あの挙動」の正体
現場でコードレビューをしていると、今でもたまに遭遇するんですよね。「非同期処理の中でループ変数が全部同じ値になってしまう」というバグ。新人エンジニアが頭を抱えて相談してくる、JavaScriptの古典的かつ非常に教育的な「沼」です。
今日は、なぜ `var` で書くと期待通りに動かず、`let` を使うと魔法のように解決するのか。その裏側にあるJavaScriptの実行モデルとスコープの深淵について、エンジニア同士の目線で深く掘り下げていきましょう。
1. なぜ `var` はループで裏切るのか?
まず、多くのエンジニアを苦しめてきた `var` の挙動を見てみましょう。
// 【問題のコード】varでループを回すと何が起きるか
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(`varの出力: ${i}`);
}, 100);
}
// 実行結果:
// varの出力: 3
// varの出力: 3
// varの出力: 3
「あれ? 0, 1, 2 と出るはずでは?」と思うかもしれません。しかし、ブラウザの実行環境(V8エンジンなど)の視点で見れば、これは極めて論理的な結果です。
`var` が抱える「共有」という呪縛
`var` は「関数スコープ」を持ちます。forループの中に書いた `var i` は、ループの外(関数全体)から参照されるたった一つの変数を使い回している状態なんです。
1. ループが回りきる(`i` が 3 になる)。
2. `setTimeout` のコールバック関数が、100ms後に実行される。
3. コールバックが `i` を参照しに行くが、その時点での `i` はすでにループを終えた「3」になっている。
つまり、`setTimeout` は「その瞬間の `i`」を保存しているのではなく、「共有されているメモリ領域の `i`」を見に行っているだけなのです。
2. `let` が提供する「ループごとの新しい世界」
対して、ES6で導入された `let` は、この問題を根本から解決しました。
// 【解決コード】letを使うと期待通りの結果に
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(`letの出力: ${i}`);
}, 100);
}
// 実行結果:
// letの出力: 0
// letの出力: 1
// letの出力: 2
なぜ `let` は成功するのか?
ここが一番面白いところです。仕様書(ECMAScript Specification)を紐解くと、`let` を使ったforループでは、「ループの反復ごとに、新しい変数バインディング(記憶領域)が生成されている」と定義されています。
裏側では、以下のようなイメージで処理が行われています。
1. 1回目のループ: `{ let i = 0; … }` という新しいスコープが生成される。
2. 2回目のループ: `{ let i = 1; … }` という別のスコープが生成される。
3. コールバック関数は、「自分が生成された時のスコープ」にある `i` を保持します。
`let` は単なる「再代入禁止の変数」ではなく、ループのたびに「その瞬間を切り取った変数」を作り直すという、非常に強力なメモリ管理を行っているのです。
3. 現場で生き残るための「ベストプラクティス」
現代のフロントエンド開発において、`var` を使う理由はほぼ皆無です。以下のルールをチームの標準にしてみてください。
- `var` は絶滅危惧種として扱う: 基本的にコードベースから排除し、`const`(再代入しない)と `let`(再代入する)の二択で考えます。
- 非同期処理とループは相性が悪いと心得る: `forEach` や `for…of` であっても、非同期処理を絡める場合は、今回の `let` の仕組みを理解していないと、意図しないクロージャの挙動に足をすくわれます。
- どうしても古い環境をサポートする場合: もしレガシーな環境で `var` しか使えない場合は、即時関数(IIFE)でスコープを切り出すのが定石でした。
// レガシーな回避策:IIFEでiを閉じ込める
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
最後に:なぜ「仕組み」を知るべきなのか
「`let` を使えばいい」という知識だけなら、Google検索のトップ記事で十分です。しかし、中級から上級へステップアップするエンジニアには、「なぜそれが動くのか」「メモリ上で何が起きているのか」を想像する力が必要です。
コードの裏側にある仕様を知ることは、単なるトラブルシューティングの効率化だけでなく、パフォーマンスを意識した設計や、より堅牢なアーキテクチャを構築するための「武器」になります。
現場でのコーディングにおいて、この `let` の挙動を意識するだけで、あなたの書く非同期処理は一気にプロフェッショナルなレベルに引き上げられます。今日からぜひ、自信を持って `let` を使いこなしてください。

コメント