フロントエンドの最前線で戦う皆さん、お疲れ様。
「`var`を使わずに`const`と`let`を使え」——これは新人の頃に誰もが叩き込まれる教訓だよね。でも、なぜそうすべきなのか、JavaScriptエンジンが内部で変数をどう「産み落とし、育てているのか」というライフサイクルまで理解しているエンジニアは、意外と少ない。
今日は、ECMAScriptの仕様書を読み解くような堅苦しい話ではなく、「ブラウザの頭の中」を覗き込むような感覚で、変数の生成から初期化、代入までの真実を紐解いていこう。
—
1. 変数のライフサイクルは「3つのフェーズ」で理解する
JavaScriptの変数は、宣言からアクセス可能になるまでに、実は3つの明確なステップを踏んでいる。
1. 生成 (Creation Phase / Declaration): スコープ内に変数を登録する。メモリを確保する準備段階。
2. 初期化 (Initialization): 変数に`undefined`を割り当て、メモリ上の場所を確定させる。
3. 代入 (Assignment): 開発者が書いた実際の値を書き込む。
この中で、`var`, `let`, `const`がどう振る舞うかが、皆さんのコードの「安定性」を左右するんだ。
var の場合:生成と初期化が「同時」に走る
`var`は非常に寛容だ。宣言する前にアクセスしてもエラーにならない(`undefined`が返る)。これは、生成と初期化がホイスティング(巻き上げ)時に同時に行われ、即座に`undefined`で埋められるからだ。
console.log(myVar); // undefined (エラーにならないのが曲者)
var myVar = “Hello”;
現場でこれがバグの温床になるのは言うまでもないよね。「変数が存在しているのに中身が空」という状態を許容してしまうからだ。
let / const の場合:死の領域「TDZ」の誕生
`let`と`const`は現代のJS開発における守護神だ。これらは、生成(宣言)と初期化の間に「Temporal Dead Zone(一時的死域:TDZ)」という強力な防壁を作る。
- 生成: スコープの先頭で変数を認識する。
- 初期化: コードがその行に到達するまで待機する。
TDZの間にアクセスすると、JavaScriptエンジンは容赦なく`ReferenceError`を吐く。 これが「宣言前の使用を禁ずる」という強力なガードレールになり、プログラミングの曖昧さを排除してくれる。
—
2. 実務で差が出る「const」の深淵
「`const`は定数だから書き換えられない」という認識は半分正解で、半分は現場レベルでは不十分だ。正確には、`const`は「値(参照先)」を固定する。
オブジェクトや配列を`const`で定義した場合、中身の書き換えは可能だよね。これはエンジンのメモリ管理において、「メモリのアドレス」を固定しているだけで、その先にあるデータ構造まではロックしていないからだ。
const user = { name: “Alice” };
// OK: プロパティの変更は可能。参照先は変わっていないから。
user.name = “Bob”;
// NG: 再代入は不可。メモリ上の参照先を書き換えようとするため。
// user = { name: “Charlie” }; // TypeError!
この挙動を理解した上で、意図的に「イミュータブル(不変)」にしたいなら `Object.freeze()` を併用するのが、シニアエンジニアの嗜みというものだ。
—
3. なぜ「let/const」だけで書くべきなのか
最後に、チーム開発におけるベストプラクティスを共有しておく。僕らが`var`を捨てた理由は単純だ。
1. スコープの明確化: `var`は関数スコープだが、`let/const`はブロック(`{}`)スコープだ。if文やfor文の中で閉じられるため、予期せぬ変数の汚染を防げる。
2. 安全性(TDZ): 宣言前にアクセスしてバグるという「JavaScript特有の事故」を、ランタイムエラーとして即座に検知できる。
3. 可読性: 「これは再代入される変数か、定数か?」をコードから読み取れる。コードはコンピュータに読ませるものだが、それ以上に「未来の自分やチームメイト」に意図を伝えるためのものだ。
実践的なTips: 綺麗なコードのための習慣
最後に、現場ですぐ使える「変数のライフサイクルを意識した書き方」のサンプルを置いておくよ。
/
- ベストプラクティス例:
- 1. 基本は const を使い、再代入が必要な場合のみ let を使う。
- 2. 意図的にスコープを区切り、変数のライフサイクルを短くする。
/
{
const API_ENDPOINT = “https://api.example.com”; // 定数は大文字、またはcamelCaseで
// 再代入が必要なカウンターなどは let で最小スコープに閉じ込める
let retryCount = 0;
const fetchData = async () => {
try {
// 処理…
} catch (e) {
retryCount++; // スコープの外側から参照せず、必要な場所で管理
}
};
}
—
結び
変数の生成から代入まで、そのライフサイクルを理解することは、単に「エラーを避ける」だけじゃない。「メモリと実行コンテキストをどう設計するか」というアーキテクチャの入り口に立っているということなんだ。
「なんとなく動く」コードから、「なぜこれで安全なのかを説明できる」コードへ。皆さんの書くコードが、より強固で美しいものになることを期待しているよ。
何かまた壁にぶつかったら、いつでも聞きに来てくれ。エンジニア同士、泥臭く積み上げていこう。

コメント