スコープと閉包 — 変数はどこから見えるか

変数が見える範囲と、関数が「作られた場所の変数を覚えている」という性質を扱います。ループの中で作った関数が全部同じ値になる有名な罠と、その原因までを実行して確かめます。

実行して検証済み処理系Node 22.22.3対応ES2015難易度●●○○○目安9更新

変数は、書かれた場所から外へは見えません。

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
外内
javascript
function show() {
const inner = '内';
}

show();

console.log(inner);
ReferenceError: inner is not defined

中からは外が見えます。逆は見えません。

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
中 2 外 1
example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
1 2 3

ReferenceError: x is not defined

{} でも区切られる

letconst は、関数だけでなくブロックでも区切られます。

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
1 2 3 1
example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
警告: 壊れた 情報: 動いた

同じ名前ですが、別の変数です。中の n は外を隠しているだけで、書き換えてはいません。

外を使いたいのに隠していると、見つけにくい間違いになります。ReferenceError: Cannot access 'x' before initialization

関数は、作られた場所を覚えている

ここが要点です。

javascript
const fns = [];

for (var i = 0; i < 3; i++) {
fns.push(() => i);
}

console.log(fns.map((f) => f()));
[ 3, 3, 3 ]
example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
[ 0, 1, 2 ]

makeCounter()もう終わっているのに、count は残っています。

返した関数が count使っているので、消えずに保たれます。 これを閉包(クロージャ)と呼びます。

呼ぶたびに別の入れ物ができる

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
最初 あとで変えた
javascript
let count = 0;

function makeCounter() {
return () => {
count++;
return count;
};
}

const a = makeCounter();
const b = makeCounter();

console.log(a(), a());
console.log(b());
1 2 3

b1 から始まります。ab は別の count を持っています。

makeCounter() を呼ぶたびに新しい count が作られ、 返した関数がそれぞれ自分のぶんを覚えています。

何に使うか

設定を閉じ込める

prefix を毎回渡さなくて済みます。 「先に決まるもの」と「あとで来るもの」を分けられます。

外から触れない状態を持つ

count は外から直接書き換えられません。 関数を通してしか変えられないので、想定外の値になりません。

いちばん有名な罠

3つとも 3 です。 0, 1, 2 にはなりません。

var で作った iループ全体で1つしかありません。 3つの関数は同じ i を覚えていて、ループが終わったときの値は 3 です。

let にすると直ります。

let繰り返しごとに新しい i を作ります。 だから、それぞれの関数が別の値を覚えます。

var を使わない理由の中で、これがいちばん実害の大きいものです。

作られる数 見える範囲
var ループ全体で1つ 関数全体
let / const 繰り返しごと ブロックの中

覚えているのは「値」ではなく「変数」

関数が覚えているのは変数そのものです。作られた時点の値ではありません。

だから、あとで変えた値が見えます。 上の var の罠も、これと同じことが起きていました。

よくある間違い

閉じ込めたつもりで共有している

count が関数のにあるので、ab が同じものを触っています。 b3 から始まりました。

閉じ込めたい変数は、関数の中で宣言してください。

まとめ

  • 変数は書かれた場所から外へは見えない。中からは外が見える
  • let / const{} でも区切られる
  • 関数は作られた場所の変数を覚えている閉包
  • 呼ぶたびに新しい入れ物ができる
  • var はループ全体で1つ。 中で作った関数が全部同じ値になる
  • 覚えているのは値ではなく変数。あとで変えた値が見える

次に学ぶ

関数を値として渡す書き方を学びます。 map() に関数を渡す形の正体、関数を返す関数、 そして「関数を受け取って新しい関数を作る」形までやります。

第3回: 関数を値として扱う

この記事の根拠

  1. Declarative Environment Records — ECMAScript® 2026 Language Specificationtc39.es
  2. CreatePerIterationEnvironment — ECMAScript® 2026 Language Specificationtc39.es
  3. クロージャ — MDNdeveloper.mozilla.org

掲載しているコード例は、公開前に読み手が押したときに動くのと同じもので実行して出力を突き合わせています。結果はリポジトリに残しています。

このページの原文(Markdown)