【実務・中級編】【初心者向け】ブロックスコープの境界線:if文やfor文の中で宣言した変数はどこまで生きているのか – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

コードレビューの現場から:その変数、本当にそこで死んでいますか?

テックリードの私が生徒や若手エンジニアのコードレビューで最も多く修正を求める箇所の一つが、実は「変数の生存期間(ライフサイクル)の管理」だ。

「とりあえず `var` で宣言しておけば動く」「関数全体の先頭でまとめて `let` を書いておくのが安全そう」――そんな感覚でコードを書いているうちは、いつまで経ってもレガシーなバグやメモリリークの温床となるスパゲッティコードから抜け出せない。

特に、UIコンポーネントの状態管理や、非同期API連携を伴うモダンなフロントエンド開発において、変数のスコープ(可視範囲)を制圧することは、バグのない堅牢なアプリケーションを作るための絶対条件だ。

今回は、JavaScriptの心臓部であるスコープのメカニズムを紐解き、`if`文や`for`文といったブロックがどのように変数の生死を分けているのか、V8エンジンのメモリ挙動まで視野に入れながら徹底解説しよう。

—

1. 関数スコープ(var)の亡霊と、ブロックスコープ(let/const)の誕生

JavaScriptが誕生した初期、この言語には `var` しか存在しなかった。`var` は「関数スコープ」を持つ。つまり、どれだけ深い `if` 文や `for` 文のブロックの中で宣言されようとも、最も近い親の「関数」のコンテキスト全体へと漏れ出す仕様になっていた。

これが何を意味するか。以下のコードを見てほしい。

function processUserData(isAdmin) {
if (isAdmin) {
var accessLevel = ‘FULL_CONTROL’; // varで宣言
}

// ブロックの外なのにアクセスできてしまう!
console.log(accessLevel); // isAdminがfalseでもエラーにならず「undefined」が出る
}

初心者だけでなく、中級者でもこの「変数のホイスティング(巻き上げ)」と関数スコープの挙動に足をすくわれる。`isAdmin` が `false` であっても、`accessLevel` という変数は関数スコープの先頭に巻き上げられ、メモリ上には存在し続けている。

モダンなブロックスコープ(let / const)の鉄則

ES6(ES2015)以降、我々は `let` と `const` を手に入れた。これらは「ブロックスコープ」を生み出す。波括弧 `{}` で囲まれた領域こそが、変数の絶対的な境界線となる。

function processUserDataModern(isAdmin) {
if (isAdmin) {
const accessLevel = ‘FULL_CONTROL’; // constでブロック内に封じ込める
console.log(accessLevel); // 正常に出力される
}

// ブロックの外からアクセスしようとすると…
// ReferenceError: accessLevel is not defined
console.log(accessLevel);
}

`const` や `let` を使った変数は、宣言されたブロックの外側からは一切参照できない。V8エンジンは、ブロックの実行が終了した瞬間に、そのスコープ内で確保されていたメモリ空間をガベージコレクションの対象としてマークしやすくなる。つまり、ブロックスコープを厳密に守ることは、メモリ効率の最適化にも直結するのだ。

—

2. 【実務の罠】for文のループと非同期処理が生むバグ

実務の現場で最も恐ろしいバグの一つが、「`var` を使ったループ処理と非同期API(`setTimeout` や `fetch` など)の組み合わせ」だ。

以下のコードを見て、何が出力されるか瞬時に分かるだろうか?

❌ アンチパターン:varでループを回す

function runBadLoop() {
for (var i = 1; i <= 3; i++) { setTimeout(function() { console.log(`現在のカウント(var): ${i}`); }, 1000 i); } } runBadLoop(); // 【実際の出力結果(1秒ごとに3回実行される)】 // 現在のカウント(var): 4 // 現在のカウント(var): 4 // 現在のカウント(var): 4 「あれ? 1, 2, 3 と出るはずが、なぜか 4 が3回出力されるぞ?」と思った読者は、スコープの基礎をもう一度復習する必要がある。 `var` は関数スコープであるため、変数 `i` はループの外(この場合はグローバル、または呼び出し元の関数)にたった1つだけしか存在しない。`setTimeout` のコールバック関数が実行される頃には、ループはすでに完了しており、`i` の値は最終的な `4` に書き換わってしまっているのだ。

⭕ 正解:letのブロックスコープが救う世界

これを `let` に書き換えるだけで、JavaScriptの挙動は劇的に変わる。

function runGoodLoop() {
for (let i = 1; i <= 3; i++) { // letは「ループの反復ごと」に新しいスコープ(変数束縛)を生成する setTimeout(function() { console.log(`現在のカウント(let): ${i}`); }, 1000 i); } } runGoodLoop(); // 【実際の出力結果】 // 現在のカウント(let): 1 (1秒後) // 現在のカウント(let): 2 (2秒後) // 現在のカウント(let): 3 (3秒後) `let` を `for` 文の初期化式で使うと、V8エンジンはループの1回ごとのイテレーション(反復)に対して、別々のメモリ領域(独立した変数 `i`)を毎回生成する。これにより、非同期コールバックがそれぞれの「その瞬間だけの `i`」を安全にクロージャとして保持できるようになるのだ。

—

3. プロダクションコードで実践する:スコープ分離による保守性向上

実際のフロントエンド開発、例えばDOM操作やイベントリスナーの登録、非同期API連携を行うコンポーネントの初期化処理において、このブロックスコープの特性をどう活かすべきか。

以下に、直感的かつメモリ効率・保守性の高い実用的なモジュールのコード例を示す。

/