こんにちは!フロントエンドからNode.jsの深部まで、日夜JavaScriptと向き合っているシニアアーキテクトです。
プログラミングを学び始めると、必ずと言っていいほど直面するのが「変数のスコープ(有効範囲)」という壁ですよね。「さっき宣言したはずの変数なのに、なぜかエラーになる…」「気づかないうちに別の変数を上書きしてしまった…」そんな悩みを抱えたことはありませんか?
今回は、JavaScriptのモダンな変数宣言である `let` と `const` が作り出す「ブロックスコープの境界線」について、一緒に深く、そして楽しく紐解いていきましょう。ここをクリアすれば、あなたの書くコードの安全性は劇的に跳ね上がりますよ。
—
1. 昔のJavaScriptの悪夢:`var` と「関数スコープ」
ブロックスコープの話をする前に、まずはJavaScriptが歩んできた歴史(そして初学者がよくハマる罠)を少しだけ知っておきましょう。
かつて、JavaScriptで変数宣言といえば `var` しかありませんでした。`var` は「関数スコープ」というルールを持っています。これは、「関数の中であれば、どこで宣言してもその関数内ならどこからでもアクセスできる」という性質です。
百聞は一見にしかず、次のコードを見てください。
function oldWay() {
if (true) {
var hazardousVariable = “私はどこにでもいます”;
}
// if文の外なのにアクセスできてしまう!
console.log(hazardousVariable); // ➔ “私はどこにでもいます” が出力される
}
oldWay();
「えっ、`if` 文のブロックの中で宣言したのに、外側から使えちゃうの?」って思いますよね。そうなんです。`var` には、私たちが普段感覚的に捉えている「波括弧 `{}` の中だけに閉じ込める」という概念がありませんでした。これが原因で、意図しない変数の書き換えや、メモリ上の予期せぬ居残り(バグの温床)が多発していたのです。
—
2. 救世主登場:`let` と `const` がもたらす「ブロックスコープ」
そこで現代のJavaScript(ES2015以降)では、`var` の代わりに `let` と `const` を使うことが絶対のスタンダードになりました。
これらが導入したのが、今回のテーマである「ブロックスコープ」です。
ブロックスコープとは?
ブロックとは、ズバリ `{ … }` (波括弧)で囲まれた領域 のことです。`if` 文の条件分岐、`for` 文や `while` 文のループ、あるいは単なる波括弧のペアもすべて「ブロック」です。
`let` や `const` で宣言した変数は、自分が生まれたその「一番近いブロック `{}`」の壁の外には一歩も出られないという厳格な寿命(ライフサイクル)を持っています。
言葉だけだと少し硬いので、具体的なコードでその境界線を覗いてみましょう。
function modernWay() {
if (true) {
let safeLet = “私はブロックの中だけの秘密”;
const safeConst = “私もうまれません外には出ません”;
console.log(safeLet); // ➔ ブロック内なので当然OK
}
// ここは if文のブロックの外側!
// console.log(safeLet); ➔ ⚠️ ReferenceError: safeLet is not defined (エラー!)
}
modernWay();
見事にエラーになりましたね!「ブロックの外に出た瞬間、変数は消滅する(メモリから解放される準備に入る)」。これがブロックスコープの境界線です。この仕組みのおかげで、変数の寿命が最小限に抑えられ、他のコードに影響を与える心配がグッと減るんです。
—
3. 実践! `for` 文で学ぶブロックスコープの威力
ブロックスコープの恩恵を最も強く受けるのが、日々のコーディングで頻出する `for` ループ です。
昔の `var` でループを書くと、ループが終わった後もカウンター変数が外側に漏れ出してしまい、思わぬバグを引き起こすことがありました。しかし、現代の `let` を使えば、その心配は一切ありません。
function loopExample() {
for (let i = 0; i < 3; i++) {
// このループの「1回ごと」に、変数 i の新しい世界(スコープ)が作られています
console.log(`ループ内: i = ${i}`);
}
// ループの外側から i を覗こうとすると…?
// console.log(i); ➔ ⚠️ ReferenceError: i is not defined
}
loopExample();
さらに、`let` や `const` をループ内で使うと、非同期処理(`setTimeout` やイベントリスナーなど)を扱う際によく起きる「あれ、変数の値が全部最後のループの数字になっちゃう!?」という有名なバグ(クロージャにまつわる問題)も、言語仕様レベルで綺麗に解決してくれます。
---
4. 陥りやすい文法エラー(TDZ:一時的死区間)
最後に、ブロックスコープや巻き上げ(Hoisting)に関連して、初心者が「なんだこれ?」とパニックになりやすいポイントを一つだけ紹介しておきます。
`let` や `const` も実はコードの実行前に「巻き上げ」自体は起こっています。しかし、`var` のように勝手に `undefined` で初期化されることはありません。宣言の行に到達するまでの間、その変数は TDZ(Temporal Dead Zone:一時的死区間) という特殊な空間に置かれます。
function tdzExample() {
// 宣言の前にアクセスしようとする
console.log(mySecret); // ➔ ⚠️ ReferenceError: Cannot access ‘mySecret’ before initialization
let mySecret = “秘密のデータ”;
}
tdzExample();
「あれ?さっきは `not defined` だったのに、今度は `Cannot access … before initialization` って言われた…?」と思ったそこのあなた、素晴らしい着眼点です!
- `not defined`: 変数がそもそも存在しない(メモリ上にない)。
- 初期化前のアクセスエラー: 変数は存在(認識)しているけれど、まだ中身を入れるコードの行に到達していないから触らせない!
JavaScriptエンジンは、変数が宣言される前にうっかり触ってしまうミスを、このTDZという仕組みで厳しくガードしてくれているのです。変数は「使う前に、必ず上のほうで宣言・初期化する」。この基本を守っていれば、このエラーに怯える必要はまったくありませんよ。
—
まとめ:変数の寿命をコントロールしよう
今回は、`let` と `const` が作り出すブロックスコープの境界線について解説しました。
- 関数スコープ (`var`): 関数内ならどこまでも変数が見えてしまうため、バグの温になりやすい。
- ブロックスコープ (`let` / `const`): `{}` の外に出たら変数は消滅する。影響範囲が狭いので安全。
- TDZ(一時적死区間): `let` / `const` は宣言前にアクセスするとガードされる。
変数の寿命をギュッと狭く、必要な場所だけに限定してあげること。これが、保守しやすくバグの少ない、美しいJavaScriptコードを書くための第一歩です。
ここをマスターしたあなたなら、もう変数まわりの挙動で迷子になることはありませんよ。自信を持って、次のコードを書きに行きましょう!