JavaScript スコープ
スコープとは、変数がどこでアクセスできるかを決める仕組みです。身分証明書のようなものだと考えてください。市区町村の証明書はその都道府県内でしか通用せず、都道府県の証明書は国外では役に立ちません。変数も同じで、スコープを出るとアクセスできなくなります。
📖 まとめ
グローバルスコープ
var、let、const で最も外側のレベルで宣言された変数はグローバルスコープを持ち、どこからでもアクセスできます。
<script>
var globalVar = "私はグローバル変数です";
function test() {
console.log(globalVar); // アクセス可能
}
test();
</script>
グローバル変数は便利ですが、名前空間を汚染しやすいです。プロジェクトが大きくなるにつれて、名前の衝突が避けられなくなります。グローバル変数の使用は最小限に抑えましょう。
関数スコープ
var で宣言された変数は関数スコープに従います。現在の関数内にのみ存在し、関数の外では消滅します。
<script>
function greet() {
var message = "こんにちは";
console.log(message); // 正常に動作
}
greet();
try {
console.log(message); // ReferenceError!
} catch(e) {
console.log("アクセス失敗: " + e.message);
}
</script>
var の関数スコープに関する典型的な落とし穴として、for ループで var を使うと、ループ終了後に変数が「漏れ出す」問題があります。
ブロックスコープ
let と const で宣言された変数はブロックスコープに従います。現在の {} 内にのみ存在します。
<script>
if (true) {
let x = 10;
const y = 20;
var z = 30;
console.log("ブロック内: x=" + x + ", y=" + y + ", z=" + z);
}
console.log("ブロック外: z=" + z); // var はブロックに制限されない
try {
console.log(x); // x にはアクセスできない
} catch(e) {
console.log("x にアクセスできません: " + e.message);
}
</script>
これが、ループカウンターに var の代わりに let を使うべき理由です。
スコープチェーン
内側のスコープは外側のスコープの変数にアクセスできます。学校で家のものを使うのと同じです(持っていさえすれば)。変数を探索する際、JavaScript は現在のスコープから外側に向かって層ごとに検索し、グローバルスコープに到達するまで続けます。この連鎖をスコープチェーンと呼びます。
<script>
const city = "東京";
function outer() {
const district = "渋谷区";
function inner() {
const street = "神宮前";
console.log(street, district, city); // すべてアクセス可能
}
inner();
}
outer();
</script>
外側のスコープは内側のスコープの変数にアクセスできません。教室の外から生徒のポケットの中身を取り出すことはできないのと同じです。
ホイスティング
var の宣言はスコープの先頭に「持ち上げられ」ますが、代入は持ち上げられません。
<script>
console.log(a); // undefined(エラーにはならない!)
var a = 5;
// 実際の実行順序: var a; → console.log(a); → a = 5;
</script>
let と const もホイスティングされますが、宣言前にアクセスするとエラーがスローされます。この領域はテンポラルデッドゾーン(TDZ)と呼ばれます。
<script>
try {
console.log(b); // ReferenceError!
} catch(e) {
console.log("テンポラルデッドゾーン: " + e.message);
}
let b = 5;
</script>
結論:常に let と const を使い、var のホイスティングの落とし穴を避けましょう。
クロージャの直感的な説明
クロージャ = 関数 + その関数が生まれた変数環境。故郷を離れた人が故郷の風景を覚えているのに似ています。故郷が後から変わっても、その人の記憶は変わりません。
<script>
function createCounter() {
let count = 0; // この変数はクロージャ内に「ロック」される
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count 変数は破壊されない。返された関数がまだ「覚えている」ため
</script>
createCounter はすでに実行を終了しています。論理的には count は破壊されるべきです。しかし、返された関数が count をしっかり保持しているため、生き残ります。これがクロージャです。
クロージャの実用的なユースケース
- カウンター:グローバル変数なしで状態を記録する(上記の例参照)
- プライベート変数:クロージャ内の変数は外側から直接アクセスできない。返された関数を通じてのみ操作可能
- コールバック関数:イベントリスナーやタイマーで変数への参照を維持する
例:ループにおける var と let の比較
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>var と let の比較</title>
<style>
body { font-family: sans-serif; padding: 20px; }
.block { margin: 16px 0; padding: 12px; background: #f5f5f5; border-radius: 6px; }
.block h3 { margin-top: 0; }
.var-result, .let-result { margin: 8px 0; font-weight: bold; }
.var-result { color: #d9534f; }
.let-result { color: #5cb85c; }
</style>
</head>
<body>
<h2>var と let:ループにおけるスコープの違い</h2>
<div id="output"></div>
<script>
const varResults = [];
const letResults = [];
for (var i = 0; i < 3; i++) {
varResults.push(`ループ内: i = ${i}`);
}
varResults.push(`ループ外: i = ${i}(var が漏れ出した!)`);
for (let j = 0; j < 3; j++) {
letResults.push(`ループ内: j = ${j}`);
}
letResults.push(`ループ外で j にアクセス → ReferenceError(let は漏れない)`);
document.getElementById("output").innerHTML = `
<div class="block">
<h3>var 宣言</h3>
${varResults.map(r => `<div class="var-result">${r}</div>`).join("")}
</div>
<div class="block">
<h3>let 宣言</h3>
${letResults.map(r => `<div class="let-result">${r}</div>`).join("")}
</div>
`;
</script>
</body>
</html>
例:スコープチェーンのデモ
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>スコープチェーン</title>
<style>
body { font-family: sans-serif; padding: 20px; }
.scope { margin: 12px 0; padding: 12px; border-left: 4px solid #4a90d9; background: #f0f7ff; border-radius: 0 6px 6px 0; }
.scope-inner { margin: 12px 0 12px 20px; padding: 12px; border-left: 4px solid #5cb85c; background: #f0fff0; border-radius: 0 6px 6px 0; }
.scope-innermost { margin: 12px 0 12px 20px; padding: 12px; border-left: 4px solid #f0ad4e; background: #fffdf0; border-radius: 0 6px 6px 0; }
code { background: rgba(0,0,0,0.06); padding: 2px 6px; border-radius: 3px; }
</style>
</head>
<body>
<h2>スコープチェーン:内側から外側へ変数を探索</h2>
<div id="output"></div>
<script>
const country = "日本";
function outer() {
const state = "東京都";
function middle() {
const city = "渋谷区";
function inner() {
const district = "神宮前";
const result = `${district}, ${city}, ${state}, ${country}`;
return result;
}
return inner();
}
return middle();
}
const final = outer();
document.getElementById("output").innerHTML = `
<div class="scope">
<strong>グローバルスコープ</strong>: country = <code>"${country}"</code>
<div class="scope-inner">
<strong>outer 関数スコープ</strong>: state = <code>"東京都"</code>
<div class="scope-innermost">
<strong>middle 関数スコープ</strong>: city = <code>"渋谷区"</code>
<div style="margin-top:12px; padding:12px; border-left:4px solid #d9534f; background:#fff0f0; border-radius:0 6px 6px 0;">
<strong>inner 関数スコープ</strong>: district = <code>"神宮前"</code>
<p>最も内側のスコープはすべての外側の変数にアクセス可能 → <strong>${final}</strong></p>
</div>
</div>
</div>
</div>
`;
</script>
</body>
</html>
例:プライベート変数を使ったクロージャカウンター
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>クロージャ</title>
<style>
body { font-family: sans-serif; padding: 20px; text-align: center; }
.counter { display: inline-block; padding: 20px; background: #f0f7ff; border-radius: 12px; margin: 12px; }
.count { font-size: 48px; font-weight: bold; color: #4a90d9; }
button { padding: 10px 24px; font-size: 16px; border: none; border-radius: 6px; cursor: pointer; background: #4a90d9; color: #fff; margin: 4px; }
button:hover { background: #357abd; }
.reset { background: #d9534f; }
.reset:hover { background: #c9302c; }
.info { color: #888; font-size: 14px; margin-top: 8px; }
</style>
</head>
<body>
<h2>クロージャカウンター</h2>
<div class="counter">
<div class="count" id="display">0</div>
<button id="increment">+1</button>
<button id="decrement">-1</button>
<button id="reset" class="reset">リセット</button>
<div class="info">count 変数はクロージャによって保護されています。外側から直接変更することはできません</div>
</div>
<script>
function createCounter() {
let count = 0;
return {
increment() { return ++count; },
decrement() { return --count; },
reset() { count = 0; return count; },
getCount() { return count; }
};
}
const counter = createCounter();
const display = document.getElementById("display");
function updateDisplay() {
display.textContent = counter.getCount();
}
document.getElementById("increment").addEventListener("click", function() {
counter.increment();
updateDisplay();
});
document.getElementById("decrement").addEventListener("click", function() {
counter.decrement();
updateDisplay();
});
document.getElementById("reset").addEventListener("click", function() {
counter.reset();
updateDisplay();
});
</script>
</body>
</html>
例:ホイスティングの比較
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ホイスティング</title>
<style>
body { font-family: sans-serif; padding: 20px; }
.compare { display: flex; gap: 20px; margin: 16px 0; }
.col { flex: 1; padding: 16px; border-radius: 8px; }
.col-var { background: #fff0f0; border: 2px solid #d9534f; }
.col-let { background: #f0fff0; border: 2px solid #5cb85c; }
h3 { margin-top: 0; }
code { background: rgba(0,0,0,0.06); padding: 2px 6px; border-radius: 3px; }
pre { background: #fff; padding: 12px; border-radius: 6px; overflow-x: auto; }
</style>
</head>
<body>
<h2>ホイスティング:var と let の比較</h2>
<div id="output"></div>
<script>
var varBefore = "var 宣言前のアクセス → " + typeof aVar;
var aVar = 5;
var varAfter = "var 宣言後のアクセス → " + aVar;
let letAfter = 5;
let letBefore = "let 宣言前のアクセス → ReferenceError がスローされる!";
document.getElementById("output").innerHTML = `
<div class="compare">
<div class="col col-var">
<h3>var(ホイスティングされるが代入されない)</h3>
<pre><code>console.log(typeof aVar); // ${varBefore}
var aVar = 5;
console.log(aVar); // ${varAfter}</code></pre>
<p>エラーにはならないが、<code>undefined</code> が返される。より巧妙なバグです!</p>
</div>
<div class="col col-let">
<h3>let(テンポラルデッドゾーン)</h3>
<pre><code>console.log(bVar); // ${letBefore}
let bVar = 5;</code></pre>
<p>即座にエラーがスローされる。問題をすぐに発見できます!</p>
</div>
</div>
`;
</script>
</body>
</html>
❓ よくある質問
for ループで var を使うと、すべてのコールバックが同じ値を出力するのはなぜですか?var は関数スコープを持つため、ループ終了後には i が1つしかなく、すべてのコールバックがそれを共有します(その時点ではループの最終値になっています)。let の場合、各反復で独立した i のコピーが作成されます。null に設定することです。let と const のどちらを使うべきですか?const を使い、再代入が必要な場合のみ let を使ってください。この習慣により、変更すべきでない変数を誤って変更するのを防げます。📝 演習
makeGreeter(greeting)関数を書きましょう。名前を引数に取り、greeting + "、" + nameを返す関数を返してください。異なる挨拶で2つのグリーティング関数を作成し、互いに干渉しないことを確認してください。createWallet(initialBalance)関数を書きましょう。deposit、withdraw、getBalanceの3つのメソッドを返してください。balanceは外側から直接アクセスできない(プライベート変数)ようにし、これらのメソッドを通じてのみ操作できるようにしてください。- 以下のコードの出力とその理由を説明してください:
<script>
for (var i = 0; i < 3; i++) {
setTimeout(function() { console.log(i); }, 100);
}
</script>
その後、0、1、2 と出力されるように修正してください。



