【実務・中級編】巻き上げ(Hoisting)の嘘と真実:関数宣言と変数宣言の優先順位 – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

巻き上げ(Hoisting)の嘘と真実:関数宣言が変数宣言よりも先にメモリに割り当てられる仕組み

コードレビューをしていて、いまだに `var` を見かけることはないだろうか。あるいは、「JavaScriptのコードは、実行前に一度上から下までスキャンされて変数や関数がファイルの一番上に引き上げられる(巻き上げられる)」という、初学者向けのファンタジーをそのまま信じていないだろうか。

テクニカルリードとして言わせてもらう。その認識のままでは、非同期処理が絡む複雑なSPAのステート管理や、クロージャを多用したモジュール設計で必ず足元をすくわれる。V8エンジンはコードを「物理的に引き上げている」わけではない。

今回は、V8の実行コンテキスト(Execution Context)の生成プロセス、特に「Creation Phase(生成フェーズ)」におけるメモリ割り当てのメカニズムを解剖し、なぜ関数宣言が変数宣言よりも優遇されるのか、そして実務でバグを踏まないための堅牢な設計パターンをロジカルに伝授する。

—

1. 実行コンテキストの裏側:V8はメモリ空間で何をしているのか

JavaScriptのコードが実行される時、V8エンジンはまず実行コンテキストを生成する。このプロセスは2つのフェーズに分かれている。

1. Creation Phase(生成フェーズ): コードが実行される前段階。スコープ内の変数や関数をスキャンし、メモリ(Lexical Environment / Variable Environment)上に領域を確保する。こここそが「巻き上げ」の正体である。
2. Execution Phase(実行フェーズ): 上から下へコードが順次実行され、値の代入や評価が行われる。

ここで重要なのは、生成フェーズにおいて、V8は「関数宣言」を「変数宣言」よりも優先してメモリにバインドするという厳格なルールを持っている点だ。

以下のコードを見てほしい。一見すると何が起きたか混乱するはずだ。

// — 衝撃の挙動:何が出力されるか? —
console.log(typeof processPayload); // 1. 出力は何になる?
console.log(typeof userId); // 2. 出力は何になる?

var userId = 42;

function processPayload() {
return “processed”;
}

var processPayload = “overwritten by variable”;

console.log(typeof processPayload); // 3. 出力は何になる?

チートシートを見ずに答えられるだろうか。
正解は順にこうなる。

1. `”function”`
2. `”undefined”`
3. `”string”`

なぜ `userId` は `undefined` なのに、`processPayload` は初期化の前にすでに「関数」として存在しているのか。そして、後から同じ名前で変数宣言(代入)された瞬間に文字列に上書きされるのか。そのメカニズムをV8の目線で紐解こう。

—

2. メモリ割り当ての優先順位と「巻き上げ」の正体

生成フェーズにおいて、V8はスコープ内の識別子を以下の順序でメモリに登録していく。

1. 関数の仮引数(Arguments)
2. 関数宣言(Function Declarations): 識別子のメモリ領域を確保し、関数オブジェクトそのものへの参照を即座にバインドする。
3. 変数宣言(Variable Declarations: `var`): 識別子のメモリ領域を確保するが、初期値はすべて `undefined` で埋められる。(※ `let` / `const` は「Temporal Dead Zone(一時的死地)」に置かれ、初期化されるまでアクセス自体がReferenceErrorになる)

つまり、「巻き上げ」とはコードが物理的に移動する現象ではなく、「実行フェーズに入る前に、メモリ空間のハッシュマップに関数や変数の枠組みが先行して登録される現象」に他ならない。

先のコードで `processPayload` が最初から関数として振る舞えたのは、生成フェーズの段階で「識別子 `processPayload` に関数実体のポインタが結びつけられたから」だ。しかし、その後の実行フェーズで `var processPayload = “overwritten by variable”` が実行された瞬間、メモリ上のポインタは文字列のプリミティブ値に書き換わる。

この挙動を理解していれば、`var` を使うことがどれほど保守性において危険な賭けであるかが身にしみてわかるはずだ。

—

3. 実務で即座に応用すべき:バグを生まない堅牢な設計パターン

プロダクション環境において、巻き上げの挙動に依存したコードを書くことは、自ら地雷原を歩くようなものだ。テクニカルリードとして、チーム全体で強制すべき設計原則を提示する。

原則①: `var` は永久に封印し、`const` と `let` で TDZ(一時的死地)を味方につける

`let` や `const` は、巻き上げは起きるものの、初期化前にアクセスすると `ReferenceError` を投げる仕様になっている。これにより、「宣言する前に使ってしまった」というバグをV8が即座に検知し、実行時エラーとしてクラッシュさせてくれる。静的解析(ESLint)と組み合わせることで、バグの芽をコンパイル/ビルドフェーズで摘むことができる。

原則②: 関数は「関数式(Function Expressions)」または「アロー関数」で定数(`const`)に代入する

関数宣言(`function foo() {}`)は巻き上げによってスコープのどこからでも呼び出せるため、コードの可読性(依存関係のフロー)を損なう原因になる。「上から下へ流れるように読める」コードベースを維持するため、関数は原則として `const` による関数式で定義する。

以下に、実務のモジュール設計において極めて安全で、V8の最適化も引き出しやすいプロダクションコードのパターンを示す。

/

  • @fileoverview ユーザーデータのフェッチとバリデーションを行う堅牢なモジュール
  • @author Technical Lead

/

‘use strict’;

// 依存モジュールや設定値は最上部に const で定義
const API_ENDPOINT = ‘https://api.example.com/v1/user’;
const MAX_RETRY_COUNT = 3;

/

  • ネットワークリクエストのラッパー(関数式による定義)
  • 巻き上げが発生しないため、定義より下流でしか呼び出せない(依存関係が明確になる)

/
const fetchWithRetry = async (url, retries = MAX_RETRY_COUNT) => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return await response.json();
} catch (error) {
if (retries > 0) {
// 再帰的なリトライ処理
console.warn(`Retrying… attempts left: ${retries}`);
return fetchWithRetry(url, retries – 1);
}
throw new Error(`Failed to fetch after max retries: ${error.message}`);
}
};

/