JavaScriptのクロージャとは?スコープと変数の覚え方を理解する

JavaScript&php

JavaScript を学ぶと、クロージャの説明で一度はつまずく人が多いです。「関数の中に関数がある」「外の変数が見えている」——文法は追えても、なぜそうなるのかが腹落ちしにくい、という声もよく聞きます。

この記事では、クロージャを「難しい概念」として暗記するのではなく、スコープの延長線上にある仕組みとして理解できるよう、基本からよくあるパターンまで順にまとめます。

この記事でわかること

  • クロージャとは何か(一言定義とイメージ)
  • レキシカルスコープとの関係
  • カウンター・ループ・コールバックなどよく出るパターン
  • 「変数が全部同じ値になる」系のつまずきポイント

こんなときに読むと役立ちます

  • クロージャという言葉は知っているが、説明できない
  • for ループの中で関数を作ると、意図と違う値になる
  • イベントリスナーやコールバックで「外の変数」が残る理由がわからない
  • 「プライベート変数」のような書き方を見て、何をしているか追いにくい

まず押さえる:クロージャとは

クロージャ(closure)とは、ざっくり言うと次のような仕組みです。

関数が、定義されたときの周囲の変数(スコープ)を「覚えたまま」動き続けること

ポイントは2つです。

  1. 関数は定義された場所のスコープを参照する(レキシカルスコープ)
  2. 外側の関数の実行が終わっても、内側の関数がその変数を保持し続けられる

最小の例

function createGreeter(name) {
  return function () {
    console.log('こんにちは、' + name);
  };
}

const greetTaro = createGreeter('太郎');
greetTaro(); // こんにちは、太郎

createGreeter の実行はすでに終わっています。それでも greetTaro は引数 name(ここでは '太郎')を覚えています。これがクロージャです。

レキシカルスコープ:どこで書いたかが大事

JavaScript では、変数をどこで参照したかではなく、関数をどこで定義したかでスコープが決まります。これをレキシカルスコープと呼びます。

const message = '外側';

function outer() {
  const message = '中側';
  function inner() {
    console.log(message); // 中側
  }
  return inner;
}

const fn = outer();
fn(); // 中側

innerouter の中で定義されているので、outer 内の message を見ます。グローバルの message ではありません。

よくあるパターン

1. カウンター(状態を閉じ込める)

外から直接いじれない「内部の数値」を持たせたいときに使われます。

function createCounter() {
  let count = 0;
  return {
    increment() { count++; },
    getCount() { return count; },
  };
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 2

count は外から直接アクセスできませんが、返したメソッドがクロージャとして count を保持しています。

2. 設定を覚えた関数を返す(ファクトリ)

function multiplyBy(factor) {
  return function (value) {
    return value * factor;
  };
}

const double = multiplyBy(2);
const triple = multiplyBy(3);

console.log(double(5));  // 10
console.log(triple(5)); // 15

doubletriple は、それぞれ別の factor を覚えています。

3. イベントやコールバック

クリック時に「そのときの値」を使いたい場面でもクロージャが効きます。

function setupButton(label) {
  const button = document.createElement('button');
  button.textContent = label;
  button.addEventListener('click', function () {
    console.log(label + ' がクリックされた');
  });
  return button;
}

コールバック関数が、定義時の label を覚えたまま、後からクリックに応答します。

つまずきポイント:ループと var

クロージャの説明でいちばん多いのが、「ループで作った関数が全部同じ値を出す」パターンです。

for (var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 100);
}
// 期待: 0, 1, 2
// 実際: 3, 3, 3

理由は、var が関数スコープでループ全体を共有し、タイマー実行時には i がすでに 3 だからです。コールバックは各回の i を個別に覚えているのではなく、同じ i を参照し続けている状態になります。

対処法1:let を使う(ブロックスコープ)

for (let i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 100);
}
// 0, 1, 2

let はループの各回ごとに別の i を作るため、コールバックが覚える値も分かれます。

対処法2:IIFE で値を固定する(昔の書き方)

for (var i = 0; i < 3; i++) {
  (function (fixed) {
    setTimeout(function () {
      console.log(fixed);
    }, 100);
  })(i);
}

即時関数に i を渡し、その回の値を fixed として閉じ込めます。

覚えておきたいこと

クロージャは「変数を覚える」仕組みなので、ループで共有されている変数をそのまま参照すると、意図とずれやすいです。let や引数でのコピーで「そのときの値」を分離する、と考えると整理しやすいです。

よくある誤解

誤解 実際
クロージャ=関数の中に関数がある 入れ子はよくあるが、本質は外側の変数を保持できること
クロージャは特別な構文 普通の関数でも、条件を満たせばクロージャになる
外側の変数はいつまでも残る 参照がなくなればガベージコレクションの対象になる

読み方のコツ

コードを見たときは、次の順で追うと理解しやすいです。

  1. 内側の関数はどこで定義されているか
  2. その外側で、どの変数・引数が使われているか
  3. 返された関数やコールバックが、いつ実行されるか(その時点で何を覚えているか)

「この関数は、生まれたときのどの変数を覚えているか?」と自分に問いかけると、クロージャの読み解きが一気に楽になります。

まとめ

  • クロージャ=関数が定義時のスコープを保持し続ける仕組み
  • スコープは定義場所で決まる(レキシカルスコープ)
  • カウンター・コールバック・設定済み関数でよく使われる
  • ループでは var の共有に注意。let や引数で値を分離する

次に function が入れ子になったコードを見たら、まず「内側は、外側のどの変数を覚えているか?」とたどってみてください。クロージャは暗記より、変数の寿命と参照の向きを追う練習で身につきます。

タイトルとURLをコピーしました