はじめに

JavaScriptを学ぶ上で、「スコープ」と「クロージャ」は避けて通れない重要な概念です。スコープは変数がどこからアクセスできるかを決定し、クロージャは関数が外部の変数を「記憶」する仕組みを提供します。

これらの概念を正しく理解することで、以下のようなメリットがあります。

  • 変数の意図しない上書きやバグを防げる
  • メモリ効率の良いコードが書ける
  • プライベートな状態を持つ関数を作成できる
  • より高度なJavaScriptパターンを理解できる

本記事では、以下の内容を初心者向けにわかりやすく解説します。

  • スコープとは何か
  • スコープの種類(グローバル・関数・ブロック)
  • スコープチェーンとレキシカルスコープ
  • クロージャの仕組みと動作原理
  • クロージャの実践的な活用例
  • ループ内でのクロージャの注意点

スコープとは

スコープの基本概念

スコープ(Scope)とは、プログラム内で変数や関数が「見える」範囲のことです。変数がスコープ内にある場合はアクセスでき、スコープ外にある場合はアクセスできません。

1
2
3
4
5
6
7
function greet() {
  const message = "こんにちは"; // この変数は関数内でのみ有効
  console.log(message); // こんにちは
}

greet();
console.log(message); // ReferenceError: message is not defined

上記の例では、message変数はgreet関数の中で宣言されているため、関数の外からはアクセスできません。これがスコープの基本的な動作です。

なぜスコープが必要なのか

スコープが存在する理由は、主に以下の3つです。

理由 説明
名前の衝突を防ぐ 異なるスコープで同じ変数名を使用しても干渉しない
セキュリティの向上 外部から意図しないアクセスを防ぐ
メモリ効率の向上 スコープを抜けると変数がガベージコレクションの対象になる

スコープの種類

JavaScriptには、主に3種類のスコープがあります。それぞれの特徴と使い方を詳しく見ていきましょう。

グローバルスコープ

グローバルスコープは、プログラム全体からアクセスできる最も広いスコープです。関数やブロックの外で宣言された変数は、グローバルスコープに属します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// グローバルスコープで宣言
const appName = "MyApp";
let userCount = 0;

function showAppInfo() {
  // グローバル変数にアクセス可能
  console.log(`アプリ名: ${appName}`);
  console.log(`ユーザー数: ${userCount}`);
}

showAppInfo();
// アプリ名: MyApp
// ユーザー数: 0

グローバル変数は便利ですが、使いすぎると名前の衝突や予期しない変更が発生しやすくなります。必要な場合を除き、グローバル変数の使用は最小限に抑えることがベストプラクティスです。

関数スコープ

関数スコープは、関数の内部でのみ有効なスコープです。関数内で宣言された変数は、その関数の外からはアクセスできません。

1
2
3
4
5
6
7
8
function calculateTax(price) {
  const taxRate = 0.10; // 関数スコープ
  const tax = price * taxRate;
  return tax;
}

console.log(calculateTax(1000)); // 100
console.log(taxRate); // ReferenceError: taxRate is not defined

varで宣言された変数は、letconstとは異なり、ブロックスコープを持たず関数スコープのみを持ちます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function example() {
  if (true) {
    var x = 10; // varは関数スコープ
    let y = 20; // letはブロックスコープ
  }
  console.log(x); // 10(アクセス可能)
  console.log(y); // ReferenceError: y is not defined
}

example();

ブロックスコープ

ブロックスコープは、ES6(ECMAScript 2015)で導入されたletconstによって実現されるスコープです。波括弧{}で囲まれたブロック内でのみ有効です。

1
2
3
4
5
6
7
if (true) {
  const blockScoped = "ブロック内のみ有効";
  let alsoBlockScoped = "これもブロック内のみ";
  console.log(blockScoped); // ブロック内のみ有効
}

console.log(blockScoped); // ReferenceError: blockScoped is not defined

ブロックスコープは、forループやif文、while文など、あらゆるブロック構造で適用されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for (let i = 0; i < 3; i++) {
  console.log(i); // 0, 1, 2
}
console.log(i); // ReferenceError: i is not defined

// varを使った場合との比較
for (var j = 0; j < 3; j++) {
  console.log(j); // 0, 1, 2
}
console.log(j); // 3(アクセス可能)

スコープの種類まとめ

各スコープの特徴を表にまとめます。

スコープ 有効範囲 対象
グローバルスコープ プログラム全体 関数・ブロック外で宣言された変数
関数スコープ 関数内 関数内で宣言されたすべての変数
ブロックスコープ ブロック内 letconstで宣言された変数

スコープチェーンとレキシカルスコープ

スコープチェーンとは

JavaScriptでは、スコープが入れ子(ネスト)になることがあります。内側のスコープから外側のスコープの変数を参照できる仕組みを「スコープチェーン」と呼びます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const global = "グローバル";

function outer() {
  const outerVar = "外側の関数";

  function inner() {
    const innerVar = "内側の関数";
    // 内側から外側のすべての変数にアクセス可能
    console.log(innerVar);  // 内側の関数
    console.log(outerVar);  // 外側の関数
    console.log(global);    // グローバル
  }

  inner();
}

outer();

スコープチェーンを図で表すと、以下のような階層構造になります。

graph TB
    A[グローバルスコープ<br/>global] --> B[outer関数スコープ<br/>outerVar]
    B --> C[inner関数スコープ<br/>innerVar]
    C -.->|参照可能| B
    C -.->|参照可能| A
    B -.->|参照可能| A

レキシカルスコープとは

レキシカルスコープ(静的スコープ)とは、関数のスコープが「関数が定義された場所」によって決まる仕組みです。関数が呼び出された場所ではなく、コード上でどこに書かれているかでスコープが決まります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const value = "グローバル";

function showValue() {
  console.log(value); // 定義時のスコープを参照
}

function wrapper() {
  const value = "wrapper内";
  showValue(); // グローバル(定義時のスコープを参照)
}

wrapper();

上記の例で、showValue関数はwrapper関数内で呼び出されていますが、valueshowValueが定義された場所(グローバルスコープ)の値を参照します。

クロージャとは

クロージャの基本

クロージャ(Closure)とは、関数とその関数が定義されたレキシカル環境(変数の参照)の組み合わせです。簡単に言えば、関数が外側のスコープの変数を「記憶」する仕組みです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function createGreeting(greeting) {
  // 内側の関数がgreeting変数を「記憶」している
  return function(name) {
    return `${greeting}${name}さん!`;
  };
}

const sayHello = createGreeting("こんにちは");
const sayGoodbye = createGreeting("さようなら");

console.log(sayHello("田中"));   // こんにちは、田中さん!
console.log(sayGoodbye("佐藤")); // さようなら、佐藤さん!

createGreeting関数の実行が終了した後も、返された内側の関数はgreeting変数にアクセスできます。これがクロージャの特徴です。

クロージャが生まれる仕組み

クロージャが生まれる条件は以下の2つです。

  1. 関数の中に別の関数が定義されている(ネストした関数)
  2. 内側の関数が外側の関数のスコープにある変数を参照している
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function outer() {
  const message = "外側の変数"; // 自由変数

  function inner() {
    console.log(message); // 外側の変数を参照
  }

  return inner; // 内側の関数を返す
}

const closureFunc = outer();
closureFunc(); // 外側の変数

outer関数が実行を終えても、message変数はinner関数のクロージャによって保持され続けます。

クロージャの動作を図解

クロージャの動作を理解するために、レキシカル環境がどのように保持されるかを図で表します。

sequenceDiagram
    participant G as グローバル
    participant O as outer関数
    participant I as inner関数(クロージャ)
    
    G->>O: outer()を呼び出し
    O->>O: message変数を作成
    O->>I: inner関数を作成(messageへの参照を保持)
    O-->>G: inner関数を返却
    Note over O: outer関数の実行終了
    G->>I: closureFunc()を呼び出し
    I->>I: 保持していたmessageを参照
    I-->>G: "外側の変数"を出力

クロージャの実践的な活用例

クロージャは、実際の開発でさまざまな場面で活用されています。ここでは、代表的な使用例を紹介します。

カウンター関数の作成

クロージャを使うと、プライベートな状態を持つ関数を作成できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function createCounter() {
  let count = 0; // プライベートな状態

  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    },
    getCount() {
      return count;
    }
  };
}

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

// count変数に直接アクセスすることはできない
console.log(counter.count); // undefined

この例では、count変数は外部から直接アクセスできず、提供されたメソッドを通じてのみ操作できます。これにより、データのカプセル化が実現できます。

関数ファクトリ

特定の設定を「記憶」した関数を生成するパターンです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function createMultiplier(multiplier) {
  return function(number) {
    return number * multiplier;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);
const tenTimes = createMultiplier(10);

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

それぞれの関数は、作成時に渡されたmultiplierの値を記憶しています。

イベントハンドラでの活用

クロージャは、イベントハンドラで特定のデータを保持する場合にも役立ちます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function setupButtons() {
  const buttons = ["保存", "削除", "編集"];

  buttons.forEach(function(buttonName) {
    const button = document.createElement("button");
    button.textContent = buttonName;
    
    // クロージャでbuttonNameを記憶
    button.addEventListener("click", function() {
      console.log(`${buttonName}ボタンがクリックされました`);
    });
    
    document.body.appendChild(button);
  });
}

setupButtons();

設定値の保持

APIクライアントなど、設定値を保持した関数を作成する例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function createApiClient(baseUrl, apiKey) {
  return {
    get(endpoint) {
      const url = `${baseUrl}${endpoint}`;
      console.log(`GET ${url}`);
      console.log(`API Key: ${apiKey}`);
      // 実際にはfetchなどでリクエストを送信
    },
    post(endpoint, data) {
      const url = `${baseUrl}${endpoint}`;
      console.log(`POST ${url}`);
      console.log(`Data: ${JSON.stringify(data)}`);
    }
  };
}

const api = createApiClient("https://api.example.com", "secret-key-123");
api.get("/users");
// GET https://api.example.com/users
// API Key: secret-key-123

ループ内でのクロージャの注意点

クロージャを使う際に、特にループ内で注意が必要なケースがあります。varを使用した場合に発生する典型的な問題を見てみましょう。

varを使った場合の問題

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 問題のあるコード
function createFunctions() {
  const functions = [];

  for (var i = 0; i < 3; i++) {
    functions.push(function() {
      console.log(i);
    });
  }

  return functions;
}

const funcs = createFunctions();
funcs[0](); // 期待: 0, 実際: 3
funcs[1](); // 期待: 1, 実際: 3
funcs[2](); // 期待: 2, 実際: 3

すべての関数が3を出力してしまいます。これは、varが関数スコープを持つため、すべてのクロージャが同じi変数を参照しているためです。

letを使った解決方法

letを使用すると、ブロックスコープによって各反復で新しい変数が作成されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 解決方法1: letを使用
function createFunctions() {
  const functions = [];

  for (let i = 0; i < 3; i++) {
    functions.push(function() {
      console.log(i);
    });
  }

  return functions;
}

const funcs = createFunctions();
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2

即時実行関数を使った解決方法

ES6以前の環境では、即時実行関数(IIFE)を使って解決していました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 解決方法2: IIFEを使用
function createFunctions() {
  const functions = [];

  for (var i = 0; i < 3; i++) {
    (function(index) {
      functions.push(function() {
        console.log(index);
      });
    })(i);
  }

  return functions;
}

const funcs = createFunctions();
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2

まとめ

本記事では、JavaScriptのスコープとクロージャについて解説しました。

スコープのポイント

  • グローバルスコープ: プログラム全体からアクセス可能
  • 関数スコープ: 関数内でのみ有効
  • ブロックスコープ: let/constで宣言された変数はブロック内でのみ有効
  • スコープチェーン: 内側のスコープから外側のスコープを参照可能
  • レキシカルスコープ: 関数のスコープは定義された場所で決まる

クロージャのポイント

  • クロージャは関数とそのレキシカル環境の組み合わせ
  • 外側のスコープの変数を「記憶」できる
  • プライベートな状態の管理に活用できる
  • ループ内で使用する場合はletを使用する

スコープとクロージャを理解することで、より安全で保守性の高いJavaScriptコードを書けるようになります。

参考リンク