【実務・中級編】【初心者向け】var・let・constの使い分け:なぜ現代のJSではvarを捨ててconstを優先すべきなのか – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

コードレビューの現場で、未だに平然と `var` が使われているコードを見かけることがある。
「動くからいいだろう」というその安易な判断が、数ヶ月後の大規模リファクタリング地獄や、非同期処理における不可解なバグ(いわゆる幽霊バグ)の温床になることに、当事者たちは気づいていない。

V8エンジンのメモリモデル、スコープチェーンの解決メカニズム、そしてモダンなフロントエンド開発における予測可能性(Predictability)の観点から、なぜ現代のJavaScriptにおいて `var` を完全に排除し、`const` をファーストチョイスとしなければならないのか。その本質を徹底的に解説しよう。

—

1. なぜ `var` は悪なのか?V8エンジンの裏側とスコープの罠

初心者向けの記事では「`var` は古いから `let` を使おう」と片付けられがちだが、問題の本質はもっと深い。`var` の最大の問題は、その「関数スコープ」と「巻き上げ(Hoisting)の仕様そのもの」にある。

関数スコープという時代遅れの仕様

`var` は、どれだけ深いブロック(`if` 文や `for` ループなど)の中で宣言されても、最も近い「関数」または「グローバル」のスコープまで這い上がる。

function processUserData(isAdmin) {
if (isAdmin) {
var accessLevel = “root”; // block内で宣言したつもり
}
console.log(accessLevel); // isAdminがfalseでもエラーにならず「undefined」が出力される
}

この挙動は、コンポーネント指向が主流の現代フロントエンド(ReactやVue、あるいはVanilla JSでのモジュール開発)において致命的だ。意図しない変数の共有や、グローバルオブジェクト(ブラウザなら `window`)の汚染を引き起こし、メモリリークや予期せぬ状態の書き換えを誘発する。

巻き上げ(Hoisting)が生む「幽霊変数」

`var` で宣言された変数は、コードの実行前にメモリ上に確保され、初期値として `undefined` が代入される。そのため、宣言より前に変数を参照してもエラーにならず、ただ値が取れないという最悪のサイレントバグを生む。

// varの危険な巻き上げ
console.log(currentUser); // ❌ エラーにならず “undefined” が出力される
var currentUser = “Alice”;

これに対し、`let` や `const` は Temporal Dead Zone(一時的死点:TDZ) というV8エンジンの安全機構の対象となり、宣言前のアクセスは厳格に `ReferenceError` をスローする。バグは「隠蔽」されるべきではなく、「即座に検知」されなければならない。

—

2. 変数宣言の黄金律:なぜ `const` を最優先すべきなのか?

モダンJSにおける変数宣言のルールは極めてシンプルだ。

1. 基本はすべて `const` で宣言する。
2. 再代入が必要であることが確実な場合のみ、`let` に格下げする。
3. `var` は一生使わない。

「すべて `const` にしたら、値を変えられないじゃないか」と思うかもしれない。しかし、ここにこそ堅牢なコードを書くための秘訣がある。

「再代入不可」がもたらす認知負荷の軽減

実務で最も開発効率を落とす要因は、「この変数の値は、この関数のどこかで書き換えられているか?」という不安を脳内でシミュレーションする時間だ。

`const` を使っていれば、その変数は「宣言された瞬間の値(あるいは参照)から一生変わらないこと」が保証される。これにより、コードのリーダブル性(保守性)が劇的に向上し、V8エンジンのオプティマイザにとっても最適化しやすいコード(イミュータブルな傾向の維持)になる。

> 注意:`const` は「値の不変(Immutability)」ではなく「再代入の禁止」を意味する。
> オブジェクトや配列を `const` で宣言した場合、プロパティの追加・変更や要素の操作は可能である点に注意せよ。

—

3. 【実務向けプロダクションコード】堅牢な状態管理とDOM操作のパターン

それでは、実際のフロントエンド開発(非同期API通信、DOM操作、配列処理)において、`const` と `let` をどのように使い分けるべきか。保守性の高い美しいコードパターンを提示する。

/

  • @file user-dashboard.js
  • @description 非同期APIからユーザーデータを取得し、UIを構築するモジュール

/

// 定数は大文字のスネークケース、またはキャメルケースで不変値として定義
const API_ENDPOINT = “https://api.example.com/v1/users”;
const DEFAULT_AVATAR = “/assets/images/default-avatar.png”;

/

  • ユーザーデータを取得してDOMをレンダリングするメイン処理
  • @param {string} userId

/
async function initializeUserDashboard(userId) {
// 再代入が必要ないDOM要素の参照はすべて const
const containerElement = document.querySelector(“#user-dashboard”);
const loaderElement = document.querySelector(“#loading-spinner”);

if (!containerElement) {
console.error(“Dashboard container element not found.”);
return;
}

try {
// ローディング表示(状態の切り替え)
loaderElement.classList.remove(“hidden”);

// APIリクエスト(レスポンスオブジェクト自体は再代入しないため const)
const response = await fetch(`${API_ENDPOINT}/${userId}`);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const userData = await response.json();

// データの加工処理(破壊的変更を避け、新しい配列/オブジェクトを生成する)
const processedUser = {
…userData,
displayName: userData.name ? userData.name.trim() : “Anonymous”,
avatarUrl: userData.avatarUrl || DEFAULT_AVATAR,
// 権限リストをイミュータブルにマッピング
permissions: userData.roles ? userData.roles.map(role => role.toUpperCase()) : []
};

// DOMの描画処理を実行
renderDashboard(containerElement, processedUser);

} catch (error) {
// エラーハンドリング
console.error(“Failed to initialize dashboard:”, error);
renderErrorMessage(containerElement, “データの読み込みに失敗しました。”);

} finally {
// 処理の終端で必ずローディングを隠す
loaderElement.classList.add(“hidden”);
}
}

/

  • ダッシュボードのHTMLを構築して描画する純粋関数
  • @param {HTMLElement} container
  • @param {Object} user

/
function renderDashboard(container, user) {
// テンプレートリテラルを用いて安全かつ宣言的にHTMLを構築
// 状態を持たないため、変数はすべて const で完結する
const permissionsHtml = user.permissions.length > 0
? user.permissions.map(perm => `${perm}`).join(“”)
: “権限なし“;

container.innerHTML = `

`;
}

/

  • XSSを防ぐための簡易エスケープ関数
  • @param {string} str
  • @returns {string}

/
function escapeHTML(str) {
// 文字列の再代入が必要なため let を使用する数少ないケース
let sanitized = str;
const escapes = {
“&”: “&”,
“<": "<", ">“: “>”,
“\””: “"”,
“‘”: “'”
};

// 置き換え処理におけるループ変数や制御用の変数は let を許容
sanitized = sanitized.replace(/[&<>“‘]/g, match => escapes[match]);
return sanitized;
}

このコードの設計美とエンジニアリングの要点

1. `var` の完全排除: ファイル全体を通じて `var` は一切存在しない。スコープの汚染や予期せぬ巻き上げが起きる余地を完全に断ち切っている。
2. 圧倒的な `const` 率: DOMの参照、APIからのレスポンス、加工後のオブジェクト、HTMLの断片に至るまで、ほとんどの変数が `const` で定義されている。これにより、「どこで値が変わったか」を追う必要が消滅している。
3. 最小限の `let`: `escapeHTML` 関数内での文字列置換など、どうしても「値を再代入しながら構築・加工していく必要がある局所的なプロセス」でのみ `let` を限定的に採用している。

—

4. チーフアーキテクトからの総括

変数宣言の選択は、単なる「コーディングスタイルの好み」ではない。それはコードの意図をV8エンジンと、そして将来のコードを読むチームメンバーに対して正確に伝えるための契約(Contract)である。

  • `const` を使うことは、「私はこの値を変化させないことで、コードの安全性を担保します」という宣言だ。
  • `let` を使うことは、「この値は動的に変化します。ただしスコープは厳格にブロック内に閉じ込めます」という警告だ。
  • `var` を使うことは、もはや現代のフロントエンド開発においては技術的負債を自ら持ち込む行為に他ならない。

今日からあなたのプロジェクトでも、すべての `var` を駆逐し、まずは `const` から書き始める習慣を徹底してほしい。コードの品質とバグの減少率に、驚くほどの違いが現れるはずだ。

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