【実務・中級編】varの巻き上げをAST解析から紐解く:コンパイルフェーズで何が起きているのか – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

こんにちは。テクニカルリードの私だ。コードレビューをしていて、未だに `var` を見かけることがある。「古いコードベースだから」「動いているからいいだろう」――そんな甘い認識が、プロダクション環境で予期せぬバグを引き起こす。

今日は、ネットによくある「`var` は巻き上げられるから使いましょう」という浅薄な説明を根本から覆し、V8エンジンが抽象構文木(AST)を生成し、Environment Recordを構築するコンパイルフェーズの深部まで潜り込む。なぜ `var` がバグの温床になり、モダンなフロントエンド設計において完全に排除されるべきなのか。そのメカニズムをロジカルに解き明かそう。

—

1. AST解析とコンパイルフェーズの裏側:`var` は何をしているのか

JavaScriptエンジン(V8など)がソースコードを実行する前段階には、必ずパース(構文解析)とコンパイルフェーズが存在する。

ソースコードはまずレキサーによってトークンに分解され、パーサーによって AST(抽象構文木:Abstract Syntax Tree) に変換される。このAST構築の段階で、エンジンはコード全体を走査し、変数のスコープを決定する。

ここで `var` と `let`/`const` の運命が決定的違いを迎える。

Function Environment Record への巻き上げの正体

`var` で宣言された変数は、現在の実行コンテキストの VariableEnvironment(関数スコープまたはグローバルスコープ)に紐づく Environment Record に登録される。

コンパイルフェーズ(実行前)において、ASTのツリーを走査したエンジンは、`var x` というノードを見つけると、以下の処理をメモリ上で先に行う。

1. スコープの Environment Record に識別子 `x` をスロットとして確保する。
2. その初期値を強制的に `undefined` で初期化する。

これが、コードの実行位置よりも前で `x` にアクセスしても `ReferenceError` にならず、`undefined` が返る(=巻き上げ・Hoistingが起きる)物理的な理由だ。

対して、ES6で導入された `let` や `const` は LexicalEnvironment に登録され、コンパイル時には値の初期化が行われない。これが「一時的死滅ゾーン(TDZ: Temporal Dead Zone)」の正体であり、初期化前にアクセスすると即座に `ReferenceError` がスローされるのは、言語仕様として安全性を担保するための必然である。

—

2. なぜ `var` はバグの温床なのか:非同期処理とスコープの罠

フロントエンド開発で最も頻発するのが、非同期API連携やイベントリスナー内でのループ変数のバグだ。`var` は関数スコープしか持たないため、ブロックスコープ(`{}`)を無視して暴走する。

以下のコードを見てほしい。一見、何気ない非同期処理のモックアップだが、ここに潜む構造的欠陥に気づくだろうか。

// 【アンチパターン】varが生み出す非同期処理の致命的バグ
function fetchUserDashboard(userIds) {
var results = [];

for (var i = 0; i < userIds.length; i++) { // 非同期APIのモック setTimeout(function() { // varはブロックスコープを持たないため、変数 i はループ外のスコープを共有する console.log(`Processing user ID at index ${i}:`, userIds[i]); results.push({ id: userIds[i], status: 'synced' }); }, 1005 (i + 1)); } // 実行直後の時点ではresultsは空(非同期のため) return results; } // 実行してみる // fetchUserDashboard([101, 102, 103]); このコードを実行すると、コンソールには以下のような意図しない出力が並ぶ。 Processing user ID at index 3: undefined Processing user ID at index 3: undefined Processing user ID at index 3: undefined

なぜこの現象が起きるのか?(V8のメモリとスコープの挙動)

1. `var i` は関数スコープ(ここでは `fetchUserDashboard` 全体)にたった1つだけ作成される。
2. ループが高速に回り切り、同期処理の段階で `i` の値は `3` に確定する。
3. 数秒後に遅れて実行される `setTimeout` のコールバック関数は、クロージャを通じて関数スコープ上の `i` を参照しに行く。その時、すでに `i` は `3` になっているため、配列の範囲外(`userIds[3]`)を参照し `undefined` を吐き出す。

—

3. プロダクションコードにおける堅牢な設計パターン

では、この問題をどう解決すべきか。答えは明快だ。`var` を一切使わず、ブロックスコープを持つ `let` や `const`、そして適切な配列メソッドを活用することだ。

さらに、モダンなフロントエンド開発においては、コールバック地獄を生む素朴なループを避け、宣言的な配列処理(`map`, `filter`, `reduce`)や `async/await` を組み合わせることで、V8の最適化エンジンにとっても効率的で、人間にとっても認知負荷の低いコードになる。

以下に、実務の現場でそのまま適用できる堅牢なプロダクションコードの模範を示す。

/

  • @fileoverview 堅牢な非同期ユーザーデータ処理モジュール
  • @author Technical Lead

/

/

  • ユーザーIDのリストを受け取り、非同期でステータスを同期する
  • @param {readonly number[]} userIds – 処理対象のユーザーID配列
  • @returns {Promise>} 同期結果の配列

/
async function syncUserDashboardSecurely(userIds) {
// 引数のイミュータビリティを担保し、予期せぬ外部変異を防ぐ
if (!Array.isArray(userIds) || userIds.length === 0) {
throw new TypeError(‘有効なユーザーIDの配列が渡されていません。’);
}

// letやconstを用いたブロックスコープの活用
// mapとPromise.allを組み合わせることで、並行処理かつスコープの汚染を防ぐ
const syncPromises = userIds.map(async (userId, index) => {
// 各反復(iteration)ごとにブロックスコープが生成されるため、
// 変数 userId や index はそのスコープ内に閉じ込められ、書き換わらない。

// ネットワーク遅延のシミュレーション(実務でのfetch API等に相当)
await new Promise(resolve => setTimeout(resolve, 100 (index + 1)));

// メモリ上の安全なオブジェクト生成
const syncResult = Object.freeze({
id: userId,
status: ‘synced’,
timestamp: Date.now()
});

console.info(`[Success] ユーザーID ${userId} の同期が完了しました。`);
return syncResult;
});

// 全ての非同期処理の完了を安全に待機
const results = await Promise.all(syncPromises);

return results;
}

// — 実行と検証 —
(async () => {
try {
const targetIds = [101, 102, 103];
const finalResults = await syncUserDashboardSecurely(targetIds);
console.log(‘最終同期結果:’, finalResults);
} catch (error) {
console.error(‘ダッシュボードの同期に失敗しました:’, error.message);
}
})();

—

4. チーフアーキテクトからの提言:なぜこの設計なのか

上記のコードには、単に `var` を排除しただけではない、プロダクション品質を保つためのアーキテクチャ上のこだわりが詰まっている。

1. ブロックスコープによる変数のカプセル化
`let` や `const` をアロー関数の引数や `map` のスコープと組み合わせることで、ループ変数が意図せず書き換えられるリスク(バグ)を根本から断ち切っている。AST解析の段階で、各ブロックごとのLexical Environment Recordが独立して生成されるため、メモリリークやスコープ汚染の心配がない。
2. イミュータビリティ(不変性)の徹底
`Object.freeze()` を用いることで、生成されたデータオブジェクトが後続の処理で意図せずミューテートされるのを防ぎ、UIコンポーネント(ReactやVueなど)の再レンダリング最適化や予測可能な状態管理(Predictable State)に寄与する。
3. パフォーマンスとV8の最適化
不要なグローバル・関数スコープの変数汚染をなくすことは、V8エンジンのガベージコレクション(GC)にとっても非常に優しい。スコープを最小限に絞ることで、不要になったメモリ空間が速やかに解放される。

まとめ

`var` の巻き上げは、JavaScriptの歴史的経緯が生んだ仕様の「歪み」に他ならない。ASTやEnvironment Recordの内部構造を知ることで、「なぜその書き方が危険なのか」「なぜモダンな仕様が強制されるのか」がロジカルに理解できたはずだ。

コードレビューで `var` を見かけたら、単に「ES6を使いましょう」と言うのではなく、「コンパイルフェーズのVariableEnvironmentとスコープ汚染の観点から、これはバグを生む」とシャープに指摘できるようになってほしい。

君たちの書くコードが、美しく、堅牢で、V8エンジンを唸らせる最高のものであることを期待している。

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