let と const とスコープ — JavaScript Tips
変数宣言は let と const が標準です。古い var はブロックスコープを持たず、巻き上げ(hoisting)の挙動も独特なため、新しいコードでは避けます。const は「再代入できない」だけで、「中身が不変」という意味ではありません。
参考: MDN — let · MDN — const · MDN — スコープ
目次
let / const / var の違い
| 宣言 | スコープ | 再代入 | 再宣言(同じ名前) |
|---|---|---|---|
let |
ブロック | 可 | 不可(同スコープ内) |
const |
ブロック | 不可 | 不可 |
var |
関数(レガシー) | 可 | 可 |
let count = 0;count = 1; // OKconst MAX = 100;// MAX = 101; // TypeError: Assignment to constant variable.var legacy = 1; // 新規コードでは使わない基本方針: デフォルトは const、再代入が必要なときだけ let を使う。
ブロックスコープ
{ } で囲んだブロック内で宣言した変数は、そのブロックの外から見えません。
{ const secret = "ブロック内"; console.log(secret); // OK}// console.log(secret); // ReferenceErrorif (true) { let temp = 42; console.log(temp);}// console.log(temp); // ReferenceErrorfor ループもブロックです。
for (let i = 0; i < 3; i++) { console.log(i);}// console.log(i); // ReferenceError(let のとき)flowchart TB subgraph outer["関数スコープ"] A["const a = 1"] subgraph block["if ブロック"] B["let b = 2"] end end block -.->|外からは見えない| X["b は参照不可"]var との対比(なぜ問題か)
if (true) { var leaked = "漏れる";}console.log(leaked); // "漏れる"(var はブロックを貫通)意図せず外側から見える変数は、名前衝突やデバッグ困難の原因になります。
TDZ(一時的デッドゾーン)
let と const は、宣言行より 前 で参照すると ReferenceError になります。この区間を TDZ と呼びます。
// console.log(message); // ReferenceError(TDZ)let message = "こんにちは";console.log(message); // OKvar は巻き上げにより宣言前でも undefined として参照できてしまい、バグの温床です。
console.log(old); // undefined(エラーにはならない)var old = 1;for ループの定番パターン
const buttons = ["A", "B", "C"];for (let i = 0; i < buttons.length; i++) { const label = buttons[i]; // 各反復で新しい i, label ができる console.log(i, label);}var i だとループ後の i が共有され、非同期コールバックと組み合わせたときに壊れやすいです。
const と let の使い分け
const は再代入不可
const PI = 3.14159;const user = { name: "花子" };user.name = "太郎"; // OK(オブジェクトの中身は変更可)// user = {}; // エラー(別オブジェクトへの再代入は不可)| ケース | 推奨 |
|---|---|
| 数値・文字列など再代入しない | const |
| ループカウンタ、累積値 | let |
| オブジェクト・配列の「箱」 | const(中身の変更は可) |
| 後から代入し直す参照 | let |
const items = [];items.push("a"); // OKlet total = 0;total += 10; // let が必要「不変にしたい」のは 参照 なのか 中身 なのかを分けて考えます。深い不変は Object.freeze や設計で扱い、日常は const + 意図した変更で十分なことが多いです。
宣言はなるべく使う直前に
function process(data) { const normalized = data.trim().toLowerCase(); let result = ""; for (const ch of normalized) { result += ch; } return result;}スコープを狭くすると、どこで何が使われているか追いやすくなります。
var を避ける理由
- ブロックスコープがない —
ifやforの中のvarが外に漏れる - 巻き上げで TDZ がない — 宣言前に
undefinedで触れてしまう - 同スコープで再宣言できる — typo で上書きに気づきにくい
function broken() { if (true) { var x = 1; } if (true) { var x = 2; // 同じ x を再宣言・上書き } console.log(x); // 2}レガシーコードを読むときだけ var に出会います。新規コードは let / const にします。
モダンな置き換えの例
// 避けたいfor (var j = 0; j < 3; j++) { setTimeout(() => console.log(j), 100);}// 3, 3, 3 と表示されがち// 推奨for (let j = 0; j < 3; j++) { setTimeout(() => console.log(j), 100);}// 0, 1, 2うまくいかないとき
| 症状 | 原因 | 対処 |
|---|---|---|
ReferenceError: Cannot access 'x' before initialization |
TDZ 内で let/const を参照 |
宣言を上に移す。宣言前に使わない |
Assignment to constant variable |
const へ再代入 |
let にするか、新しい変数名を使う |
| ループのコールバックがおかしい | var でインデックス共有 |
let または for...of |
| ブロック外で変数が見えない | ブロックスコープが効いている | 必要ならブロック外で宣言(ただしスコープは広げすぎない) |
const なのに配列を変更できる |
const は参照の固定のみ |
意図どおりなら問題なし。完全凍結は別手段 |
関連: MDN — var · MDN — クロージャ