【入門編】関数宣言と関数式の巻き上げ優先順位:ASTから読み解く解析順序と実行時の挙動 – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

こんにちは!日々の開発、本当にお疲れ様です。

JavaScriptを書き進めていく中で、「あれ、変数や関数を宣言したはずなのに、なぜか `undefined` になる……」「同じ名前をつけたら、どっちが優先されるんだっけ?」と頭を悩ませた経験はありませんか?

他の言語からやってきた方であればなおさら、JavaScriptの不思議な挙動に戸惑うことでしょう。でも、安心してください。ここをクリアすれば、JavaScriptのコードがエンジン内部でどう解釈されているのかが手に取るように分かり、バグの予兆を脳内でスイスイとデバッグできるようになりますよ。

今回は、JavaScriptの「巻き上げ(Hoisting)」の核心、そして関数宣言と変数宣言が衝突したときの優先順位について、エンジン内部の視点(抽象構文木:ASTと実行コンテキスト)を交えながら、優しく、そして深く紐解いていきましょう。

—

1. そもそも「巻き上げ(Hoisting)」って何?

JavaScriptのエンジン(V8など)は、コードを上から1行ずつ上から順番に実行しているわけではありません。実は、コードを実行する前に「作成フェーズ(コンパイル段階)」という秘密の準備期間を設けています。

この準備期間中に、エンジンはソースコード全体をスキャンし、`var` や `let`、`const`、そして `function` といった宣言をコードの先頭に「引き上げた」かのように記憶します。これが「巻き上げ」の正体です。

イメージとしては、コードを実行する前に、エンジンの脳内メモ帳にこのようなリストが作られる感じです。

// 【脳内イメージ:エンジンの事前準備】
// 「おっ、ここに greet という関数があって、x という変数があるな。先にメモしておこう」

この仕組みを知らないと、「まだ宣言していないはずの関数や変数が呼び出せる?!」という不可解な現象に直面することになります。

—

2. 【本題】関数宣言 vs 変数宣言の衝突!勝つのはどっち?

さて、ここからが本題です。もし、「同じ名前」の関数宣言と変数宣言が同時に現れたら、JavaScriptエンジンはどちらを優先するのでしょうか?

次のコードを見て、何が出力されるか予想してみてください。

// さあ、何が出力されるでしょうか?
console.log(typeof magic);

var magic = “私は変数です”;

function magic() {
return “私は関数です”;
}

console.log(typeof magic);

……正解は、最初の `console.log(typeof magic)` では `”function”` が出力され、最後の `console.log(typeof magic)` では `”string”` が出力されます。

「えっ、変数に文字列を代入したのに、最初の段階では関数扱いになっているの?」と驚きましたよね。ここには、JavaScriptの仕様に裏打ちされた明確な優先順位のルールが存在します。

エンジンが解釈する優先順位の3ステップ

JavaScriptエンジンがスコープを解析する際、宣言は以下の厳格な優先順位でメモリ(変数オブジェクト / 環境レコード)に登録されます。

1. 第1位:関数引数(Arguments)
2. 第2位:関数宣言(Function Declaration) 👑 【ここが最強クラス!】
3. 第3位:変数宣言(Variable Declaration with `var`)

そう、関数宣言は、`var` による変数宣言よりも優先順位が高いのです。

先ほどのコードがエンジンの内部でどう解釈されているか、実際のAST(抽象構文木)の解析プロセスを模してコードの形に戻してみましょう。エンジンは、準備フェーズを終えた時点で以下のようにコードを脳内変換しています。

// 【JavaScriptエンジン内部の解釈(巻き上げ後の姿)】

// 1. まず、優先度の高い「関数宣言」が丸ごと巻き上げられる
function magic() {
return “私は関数です”;
}

// 2. 次に、「変数宣言(var magic)」が巻き上げられるが、
// すでに同名の `magic` が関数として存在するため、この宣言は「無視(上書きされない)」される!
// var magic;

// — ここから実行フェーズ —

console.log(typeof magic); // すでに関数として存在しているので “function”

magic = “私は変数です”; // ここで初めて値が「文字列」に書き換わる

console.log(typeof magic); // 書き換わったので “string”

関数宣言は、その「中身(関数本体)」ごとスコープのトップに引き上げられます。一方、`var` による変数宣言は、すでに同名の関数が存在する場合、その存在にかき消されて単に無視されてしまうのです。これが、「なぜ最初に `function` として扱われるのか」の答えです。

—

3. `let` や `const` を使った場合の挙動

「じゃあ、最近のモダンな書き方である `let` や `const` を使ったらどうなるの?」気になりますよね。

実は、現代のJavaScript(ES6以降)において、`let` や `const` は `var` とは異なる挙動をします。同じ名前で衝突させようとすると、巻き上げの仕組みそのものが変わります。

let target = “変数”;

// 以下のコードを書くとどうなるでしょうか?
function target() {
return “関数”;
}
// 🔴 構文エラー (SyntaxError: Identifier ‘target’ has already been declared)

`let` や `const` は、同じスコープ内で同じ名前の識別子を二重に宣言することを絶対に許しません。そのため、エンジンがパース(構文解析)した瞬間に `SyntaxError` を吐き出し、コードの実行すらさせてくれません。

これは非常に安全な挙動です。なぜなら、意図しない変数と関数の衝突をコンパイル段階で完全に防いでくれるからです。

—

4. 現場で絶対に避けるべき「アンチパターン」とベストプラクティス

ここまで関数と変数の巻き上げ優先順位について深く解説してきましたが、実際の開発現場では、この仕様に依存したコードを書くべきではありません。

「巻き上げがあるから、関数はファイルのどこに書いても動くぜ!」と油断していると、次のようなバグの温床になります。

❌ 避けるべき書き方(意図が読めないコード)

// どこで何が定義されているか追いかけにくい
doSomething();

var doSomething = function() {
console.log(“変数に代入された関数式”);
};
// ⚠️ TypeError: doSomething is not a function
// (関数式の場合、変数の `undefined` だけが巻き上げられるため、関数として実行できずエラーになる!)

※そう、関数を「変数に代入する形(関数式)」で書いた場合、巻き上げられるのは変数名だけであり、中身の関数は巻き上げられません。 ここも初学者が一番ハマりやすい罠です。

⭕ マスターが実践する美しい書き方

プログラミングの黄金律は、「使う前に宣言する」ことです。

1. 関数宣言よりも、まずは「関数式」や「アロー関数 (`const`)」を主軸に使う
2. コードは常に、上から下へと自然に読める順序で配置する
3. `var` は一切使わず、`const` と `let` でスコープを厳格に管理する

// ベストプラクティス:アロー関数を const で宣言し、使用する前に定義する
const calculateTotal = (price, tax) => {
return price (1 + tax);
};

const finalPrice = calculateTotal(1000, 0.1);
console.log(finalPrice); // 1100

このように書いておけば、巻き上げの優先順位に頭を悩ませる必要もなければ、V8エンジンや他の開発者を無駄に混乱させることもありません。

—

まとめ

  • 巻き上げとは、実行前の準備フェーズで宣言がスコープの先頭に引き上げられる現象。
  • 優先順位の勝者は「関数宣言」。同名の `var` 変数宣言よりも強く、関数の中身ごとトップに陣取る。
  • ただし、この仕様に頼るのはバグの元。現代のモダンJSでは `const` / `let` を使い、「使う前に宣言する」を徹底しよう。

ここをしっかりと理解できたあなたなら、もう変数やスコープ周りで迷子になることはありません。この知見を武器に、ぜひ自信を持ってクリーンで堅牢なコードを書き進めてくださいね。あなたのJavaScriptライフが、さらに素晴らしいものになるよう応援しています!

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