2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

新・JavaScript文法(6):スコープとクロージャ

Last updated at Posted at 2025-06-14

前回の記事 では、JavaScriptの関数について、従来の関数宣言から最新のアロー関数までを扱いました。今回は、JavaScriptのスコープとクロージャについて、基本的な概念から実践的な活用方法までを扱います。

スコープの概念

スコープとは、変数や関数が参照できる範囲のことです。スコープの理解は、バグの少ない安全なコードを書くために欠かせません。

グローバルスコープとローカルスコープ

// グローバルスコープの変数
let globalVar = "グローバル変数";

function outerFunction() {
  // 関数スコープ(ローカルスコープ)の変数
  let localVar = "ローカル変数";

  console.log(globalVar); // "グローバル変数" - アクセス可能
  console.log(localVar);  // "ローカル変数" - アクセス可能

  function innerFunction() {
    // より内側のスコープ
    let innerVar = "内側の変数";

    console.log(globalVar); // "グローバル変数" - アクセス可能
    console.log(localVar);  // "ローカル変数" - アクセス可能
    console.log(innerVar);  // "内側の変数" - アクセス可能
  }

  innerFunction();
  // console.log(innerVar); // エラー: innerVar is not defined
}

outerFunction();
// console.log(localVar); // エラー: localVar is not defined

ブロックスコープ

ES2015(ES6)以降、letconst により、ブロックスコープが導入されました。

// varの場合(関数スコープ)
function varExample() {
  if (true) {
    var x = 1;
  }
  console.log(x); // 1 - ブロック外でもアクセス可能
}

// let/constの場合(ブロックスコープ)
function letExample() {
  if (true) {
    let y = 1;
    const z = 2;
  }
  // console.log(y); // エラー: y is not defined
  // console.log(z); // エラー: z is not defined
}

// forループでの違い
function loopExample() {
  console.log('=== var を使った場合の問題 ===');
  // varの場合:関数スコープのため、ループ後の最終値が参照される
  for (var i = 0; i < 3; i++) {
    setTimeout(() => {
      console.log("var:", i); // すべて 3 が表示される
    }, 100);
  }
  console.log('ループ終了後のvar i:', i); // 3 (ループ外からもアクセス可能)

  console.log('=== let を使った場合の解決 ===');
  // letの場合:ブロックスコープのため、各反復で新しいバインディングが作成される
  for (let j = 0; j < 3; j++) {
    setTimeout(() => {
      console.log("let:", j); // 0, 1, 2 が表示される
    }, 150);
  }
  // console.log('ループ終了後のlet j:', j); // エラー: j is not defined
}

loopExample();

レキシカルスコープ(静的スコープ)

JavaScriptは「レキシカルスコープ」(静的スコープ)を採用しています。これは、一般的には「変数の参照先が関数の定義時点で決まる」と説明されている仕組みです。

let message = "グローバルメッセージ";

function outer() {
  let message = "外側のメッセージ";

  function inner() {
    console.log(message); // "外側のメッセージ"
  }

  return inner;
}

function anotherFunction() {
  let message = "別の関数のメッセージ";
  let innerFunc = outer(); // inner関数を取得
  innerFunc(); // "外側のメッセージ" - 定義された場所のスコープを参照
}

anotherFunction();

上記の例について補足します。

  1. anotherFunction() が呼び出されている
  2. anotherFunction() 内で innerFunc() が呼び出されている。これは、すぐ上の行により outer() 関数が実行されることになる
  3. outer() 関数は return inner; により inner() 関数が返される
  4. inner() 関数は message を出力する。この messageinner() 内では定義されていない。しかし、outer() 関数のスコープ内で定義されているため、outer()message が参照される。よって、"外側のメッセージ" が出力される

「変数の参照先が関数の定義時点で決まる」という説明は難しく感じるものですが、上記の例のようにレキシカルスコープは「関数が定義された場所のスコープを参照できる」という仕組みです。inner()outer() で定義されているため、inner() 内から outer() のスコープにアクセスできます。

アロー関数と通常の関数での this の違い

レキシカルスコープは this の扱いにも影響します。アロー関数は this をレキシカルにバインド(特定のオブジェクトへ紐づけ)するため、定義された時点での this を保持します。

const person = {
  name: "田中",

  // 通常の関数の場合
  greetNormal: function() {
    console.log(`こんにちは、${this.name}さん`); // "こんにちは、田中さん"

    setTimeout(function() {
      // 通常の関数では this が変わってしまう(globalオブジェクトやundefined)
      console.log(`遅延: ${this.name}さん`); // "遅延: undefinedさん"
    }, 100);
  },

  // アロー関数を使った解決法
  greetArrow: function() {
    console.log(`こんにちは、${this.name}さん`); // "こんにちは、田中さん"

    setTimeout(() => {
      // アロー関数では定義時点のthisが保持される
      console.log(`遅延: ${this.name}さん`); // "遅延: 田中さん"
    }, 100);
  },

  // 従来の解決法(参考)
  greetBind: function() {
    console.log(`こんにちは、${this.name}さん`);

    setTimeout(function() {
      console.log(`遅延: ${this.name}さん`); // "遅延: 田中さん"
    }.bind(this), 100); // .bind(this)でthisを明示的にバインド
  }
};

// 実行例
person.greetNormal(); // undefinedが表示される
person.greetArrow();  // 正しく"田中"が表示される
person.greetBind();   // 正しく"田中"が表示される(従来の方法)

クロージャの理解と活用

クロージャは、関数が定義されたときのスコープの変数にアクセスできる仕組みです。この機能により、データの隠蔽や状態の保持が可能になります。

以下は、クロージャの重要なポイントです。

  1. データの隠蔽
    外部から直接アクセスできないプライベートな変数を作成
  2. 状態の保持
    関数が「記憶」を持つことで、呼び出し間で情報を保持
  3. メモリ使用
    クロージャは参照される変数をメモリに保持し続けるため、適切な管理が必要

基本的なクロージャの例

function createCounter() {
  let count = 0; // プライベートな変数

  return function() {
    count++; // 外側の変数にアクセス
    console.log(`カウント: ${count}`);
    return count;
  };
}

const counter1 = createCounter();
const counter2 = createCounter();

counter1(); // "カウント: 1"
counter1(); // "カウント: 2"
counter1(); // "カウント: 3"

counter2(); // "カウント: 1" - 独立したカウンター
counter2(); // "カウント: 2"

// count変数に直接アクセスはできない
// console.log(count); // エラー: count is not defined

より実践的なクロージャの例

// 設定可能なカウンター
function createConfigurableCounter(initialValue = 0, step = 1) {
  let count = initialValue;

  return {
    increment() {
      count += step;
      return count;
    },

    decrement() {
      count -= step;
      return count;
    },

    getValue() {
      return count;
    },

    reset() {
      count = initialValue;
      return count;
    }
  };
}

const counter = createConfigurableCounter(10, 5);
console.log(counter.increment()); // 15
console.log(counter.increment()); // 20
console.log(counter.decrement()); // 15
console.log(counter.getValue());  // 15
console.log(counter.reset());     // 10

関数ファクトリーパターン

// 特定の挨拶を生成する関数ファクトリー
function createGreeter(greeting) {
  return function(name) {
    return `${greeting}${name}さん!`;
  };
}

const sayHello = createGreeter("こんにちは");
const sayGoodMorning = createGreeter("おはようございます");
const sayGoodEvening = createGreeter("こんばんは");

console.log(sayHello("山田"));        // "こんにちは、山田さん!"
console.log(sayGoodMorning("佐藤"));  // "おはようございます、佐藤さん!"
console.log(sayGoodEvening("田中"));  // "こんばんは、田中さん!"
// 計算機能を生成する関数ファクトリー
function createCalculator(operation) {
  return function(a, b) {
    switch (operation) {
      case 'add':
        return a + b;
      case 'subtract':
        return a - b;
      case 'multiply':
        return a * b;
      case 'divide':
        return b !== 0 ? a / b : 'ゼロ除算エラー';
      default:
        return '不明な演算';
    }
  };
}

const add = createCalculator('add');
const multiply = createCalculator('multiply');

console.log(add(5, 3));      // 8
console.log(multiply(4, 7)); // 28

モジュールパターン

クロージャを活用したモジュールパターンは、プライベートな変数やメソッドを持つオブジェクトを作成するために使われます。

IIFE(即時関数)を使ったモジュールパターン

// IIFE (Immediately Invoked Function Expression) を使った例
const userModule = (function() {
  // プライベート変数
  let users = [];
  let currentId = 1;

  // プライベートメソッド
  function generateId() {
    return currentId++;
  }

  function validateUser(user) {
    return user.name && user.name.length > 0;
  }

  // パブリックAPIを返す
  return {
    addUser(name, email) {
      const user = {
        id: generateId(),
        name: name,
        email: email,
        createdAt: new Date()
      };

      if (validateUser(user)) {
        users.push(user);
        return user;
      } else {
        throw new Error('無効なユーザーデータです');
      }
    },

    getUser(id) {
      return users.find(user => user.id === id);
    },

    getAllUsers() {
      // プライベート配列のコピーを返す(外部からの変更を防ぐ)
      return users.map(user => ({ ...user }));
    },

    getUserCount() {
      return users.length;
    },

    removeUser(id) {
      const index = users.findIndex(user => user.id === id);
      if (index !== -1) {
        return users.splice(index, 1)[0];
      }
      return null;
    }
  };
})();

// 使用例
try {
  const user1 = userModule.addUser("山田太郎", "yamada@example.com");
  const user2 = userModule.addUser("佐藤花子", "sato@example.com");

  console.log("ユーザー数:", userModule.getUserCount()); // "ユーザー数: 2"
  console.log("ユーザー1:", userModule.getUser(1));

  // プライベート変数には直接アクセスできない
  console.log(userModule.users); // undefined
  console.log(userModule.currentId); // undefined
} catch (error) {
  console.error(error.message);
}

ES Moduleとの対比

以下は、IIFEパターンとES Moduleの違いを示す例です。IIFEはモジュール化のための古典的な方法であり、ES Moduleは最新のJavaScriptで推奨される方法です。

// 従来のIIFEパターン
const mathUtils = (function() {
  const PI = 3.14159;

  function square(x) {
    return x * x;
  }

  return {
    area: (radius) => PI * square(radius),
    circumference: (radius) => 2 * PI * radius
  };
})();

// ES Module形式(参考)
/*
// mathUtils.js
const PI = 3.14159;

function square(x) {
  return x * x;
}

export const area = (radius) => PI * square(radius);
export const circumference = (radius) => 2 * PI * radius;

// 使用する側
import { area, circumference } from './mathUtils.js';
*/

クロージャの注意点

メモリリークの防止

クロージャは不適切に使用するとメモリリークの原因となることがあります。

悪い例:不要な大きなデータをクロージャが参照し続けてしまう

// この例では、largeDataは実際には使われていないが、
// クロージャが参照しているためメモリが解放されない
function badExample() {
  const largeData = new Array(1000000).fill('large data'); // 大きなデータ

  return function(input) {
    // largeDataを使っていないのに、クロージャがlargeDataを保持し続ける
    return input * 2;
  };
}

良い例:必要な情報だけを変数に取り出してからクロージャを返す

// 必要な情報だけを変数に取り出し、クロージャで参照するのはその値だけにする
// これでlargeData自体はGCの対象になる
function goodExample() {
  const largeData = new Array(1000000).fill('large data');
  const dataLength = largeData.length; // 必要な部分だけ取り出す

  return function(input) {
    // ここではdataLengthだけを参照
    return input * dataLength;
  };
}

さらに良い例:不要になったら明示的にクロージャの中身をクリアできる仕組み

// disposeメソッドでクロージャ内の変数をクリアし、メモリを解放できるようにする
function createCleanableCounter() {
  let count = 0;
  let isActive = true;

  const counter = function() {
    if (!isActive) {
      throw new Error('カウンターは無効化されています');
    }
    return ++count;
  };

  // クリーンアップ機能
  counter.dispose = function() {
    count = null; // 参照を切る
    isActive = false;
  };

  return counter;
}

// 使用例
const counter = createCleanableCounter();
console.log(counter()); // 1
console.log(counter()); // 2

// 不要になったらクリーンアップ
counter.dispose();
// counter(); // エラー: カウンターは無効化されています

クロージャを使う際のポイント

サンプルコードで上記、説明した内容について、まとめます。

  1. 必要最小限の変数のみを参照する
  2. 大きなオブジェクトへの参照は避ける
  3. 不要になったらnullを代入してガベージコレクションを促進
  4. 開発者ツールでメモリ使用量を監視する

実践的なコード例

以下では、実際の開発で役立つクロージャの活用例を紹介します。

1. イベントハンドラーでの状態管理

function createClickCounter(elementId, options = {}) {
  const element = document.getElementById(elementId);
  let clickCount = 0;
  const { maxClicks = Infinity, onMaxReached = null } = options;

  if (!element) {
    console.error(`要素が見つかりません: ${elementId}`);
    return null;
  }

  // イベントハンドラー関数(クロージャ)
  function handleClick() {
    if (clickCount >= maxClicks) {
      if (onMaxReached) {
        onMaxReached(clickCount);
      }
      return;
    }

    clickCount++;
    this.textContent = `クリック回数: ${clickCount}`;

    // 最大回数に達した場合の処理
    if (clickCount >= maxClicks && onMaxReached) {
      onMaxReached(clickCount);
    }
  }

  element.addEventListener('click', handleClick);

  // 公開API
  return {
    getCount() {
      return clickCount;
    },

    reset() {
      clickCount = 0;
      element.textContent = 'クリック回数: 0';
    },

    setMaxClicks(max) {
      maxClicks = max;
    },

    // メモリリーク防止のための後始末
    destroy() {
      element.removeEventListener('click', handleClick);
      element.textContent = '';
    }
  };
}

// 使用例(HTMLに <button id="myButton">クリックしてください</button> がある場合)
const clickCounter = createClickCounter('myButton', {
  maxClicks: 5,
  onMaxReached: (count) => {
    alert(`最大クリック数 ${count} に達しました!`);
  }
});

// エラーハンドリングの例
if (clickCounter) {
  console.log('現在のカウント:', clickCounter.getCount());

  // 5秒後にリセット
  setTimeout(() => {
    clickCounter.reset();
  }, 5000);
}

2. キャッシュ機能付き関数(メモ化)

function createMemoizedFunction(fn) {
  const cache = new Map();

  return function(...args) {
    // 引数をキーとして使用(オブジェクトや配列の場合は注意が必要)
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      console.log('キャッシュから取得:', key);
      return cache.get(key);
    }

    console.log('計算中:', key);
    try {
      const result = fn.apply(this, args);
      cache.set(key, result);
      return result;
    } catch (error) {
      // エラーが発生した場合はキャッシュしない
      console.error('計算エラー:', error.message);
      throw error;
    }
  };
}

// フィボナッチ数列の計算関数(再帰的実装)
function fibonacci(n) {
  if (n < 0) throw new Error('負の数は処理できません');
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// メモ化されたフィボナッチ関数
const memoizedFibonacci = createMemoizedFunction(fibonacci);

// 実行例とパフォーマンス測定
console.time('1回目');
console.log(memoizedFibonacci(40)); // 初回は計算に時間がかかる
console.timeEnd('1回目');

console.time('2回目');
console.log(memoizedFibonacci(40)); // キャッシュから瞬時に取得
console.timeEnd('2回目');
// キャッシュクリア機能付きのより高機能な例(LRU対応)
// LRU: 最も長く使われていないものから削除
function createAdvancedMemoizer(fn, maxCacheSize = 100) {
  const cache = new Map();

  const memoized = function(...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      // アクセス順序を更新(LRU対応)
      const value = cache.get(key);
      cache.delete(key);
      cache.set(key, value);
      return value;
    }

    // キャッシュサイズ制限(追加前にチェック)
    if (cache.size >= maxCacheSize) {
      // Mapの先頭(最も古い)エントリを削除
      const firstKey = cache.keys().next().value;
      cache.delete(firstKey);
    }

    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };

  // キャッシュ管理メソッド
  memoized.clearCache = () => cache.clear();
  memoized.getCacheSize = () => cache.size;

  return memoized;
}

// --- 利用例 ---
// 計算回数を確認するための関数
function slowSquare(n) {
  console.log(`計算: ${n} * ${n}`);
  return n * n;
}

const lruMemoSquare = createAdvancedMemoizer(slowSquare, 3);

console.log(lruMemoSquare(2)); // 計算: 2 * 2 → 4
console.log(lruMemoSquare(3)); // 計算: 3 * 3 → 9
console.log(lruMemoSquare(4)); // 計算: 4 * 4 → 16
console.log(lruMemoSquare(2)); // キャッシュから取得 → 4
console.log(lruMemoSquare(5)); // 計算: 5 * 5 → 25(最も古い3が削除される)
console.log(lruMemoSquare(3)); // 計算: 3 * 3 → 9(キャッシュから消えているので再計算)

console.log('キャッシュサイズ:', lruMemoSquare.getCacheSize()); // 3
lruMemoSquare.clearCache();
console.log('キャッシュクリア後:', lruMemoSquare.getCacheSize()); // 0

3. 設定管理モジュール

const configManager = (function() {
  const config = {};
  const defaults = {
    theme: 'light',
    language: 'ja',
    autoSave: true,
    maxRetries: 3
  };

  // 初期化
  Object.assign(config, defaults);

  return {
    get(key) {
      return config[key];
    },

    set(key, value) {
      const oldValue = config[key];
      config[key] = value;

      // 設定変更を通知(イベント的な処理)
      console.log(`設定変更: ${key} = ${oldValue}${value}`);

      return this; // メソッドチェーンを可能にする
    },

    getAll() {
      return { ...config }; // コピーを返す
    },

    reset(key) {
      if (key) {
        config[key] = defaults[key];
      } else {
        Object.assign(config, defaults);
      }
      return this;
    },

    has(key) {
      return key in config;
    }
  };
})();

// 使用例
configManager
  .set('theme', 'dark')
  .set('language', 'en')
  .set('maxRetries', 5);

console.log('現在のテーマ:', configManager.get('theme')); // "dark"
console.log('全設定:', configManager.getAll());

復習

基本問題

1. 以下のコードの出力結果を予想する(理由も考える)

function outerFunc() {
  let x = 10;

  function innerFunc() {
    console.log(x);
  }

  x = 20;
  return innerFunc;
}

const myFunc = outerFunc();
myFunc();

2. 次のコードでカウンターが正しく動作しない理由を考え、修正する

for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log("カウンター:", i);
  }, 100);
}

3. クロージャを使って、初期値と増分値を設定できるカウンター関数を作成する

// 使用例
// const counter1 = createCounter(5, 2); // 初期値5、増分2
// console.log(counter1()); // 7
// console.log(counter1()); // 9

実践問題

4. プライベートな配列を持ち、以下のメソッドを提供するモジュールを作成する

  • add(item): アイテムを追加
  • remove(item): アイテムを削除
  • getAll(): 全アイテムを取得(コピーを返す)
  • size(): アイテム数を取得
  • clear(): 全アイテムを削除

5. 関数の実行回数を制限するクロージャを作成する

// 使用例
// const limitedFunc = createLimitedFunction(myFunction, 3);
// limitedFunc(); // 実行される
// limitedFunc(); // 実行される
// limitedFunc(); // 実行される
// limitedFunc(); // 実行されない(制限に達したため)

解答例

問題1

// 出力: 20
// 理由: innerFuncが実行される時点で、xは20に変更されている。
// クロージャは変数の参照を保持するため、最新の値が使用される。

問題2

// 問題: varは関数スコープのため、ループ終了後のiの値(4)が参照される

// 修正案1: letを使用
for (let i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log("カウンター:", i);
  }, 100);
}

// 修正案2: クロージャを使用
for (var i = 1; i <= 3; i++) {
  (function(j) {
    setTimeout(function() {
      console.log("カウンター:", j);
    }, 100);
  })(i);
}

問題3

function createCounter(initialValue = 0, step = 1) {
  let count = initialValue;

  return function() {
    count += step;
    return count;
  };
}

// テスト
const counter1 = createCounter(5, 2);
console.log(counter1()); // 7
console.log(counter1()); // 9

const counter2 = createCounter(); // デフォルト値使用
console.log(counter2()); // 1
console.log(counter2()); // 2

問題4

const listModule = (function() {
  let items = [];

  return {
    add(item) {
      items.push(item);
      return this;
    },

    remove(item) {
      const index = items.indexOf(item);
      if (index !== -1) {
        items.splice(index, 1);
      }
      return this;
    },

    getAll() {
      return [...items]; // コピーを返す
    },

    size() {
      return items.length;
    },

    clear() {
      items = [];
      return this;
    }
  };
})();

// テスト
listModule.add('りんご').add('みかん').add('バナナ');
console.log(listModule.getAll()); // ['りんご', 'みかん', 'バナナ']
console.log(listModule.size());   // 3
listModule.remove('みかん');
console.log(listModule.getAll()); // ['りんご', 'バナナ']

問題5

function createLimitedFunction(fn, maxCalls) {
  let callCount = 0;

  return function(...args) {
    if (callCount < maxCalls) {
      callCount++;
      console.log(`実行回数: ${callCount}/${maxCalls}`);
      return fn.apply(this, args);
    } else {
      console.log('実行制限に達しました');
      return undefined;
    }
  };
}

// テスト
function greet(name) {
  console.log(`こんにちは、${name}さん!`);
}

const limitedGreet = createLimitedFunction(greet, 2);
limitedGreet('太郎'); // 実行される
limitedGreet('花子'); // 実行される
limitedGreet('次郎'); // 実行されない

まとめ

JavaScriptのスコープとクロージャの概念を理解することで、以下のような利点があります。

技術的メリット

  1. データのカプセル化
    プライベートな変数や関数を作成し、外部からの不正アクセスを防ぐ
  2. 状態の管理
    関数が状態を「記憶」し、後続の呼び出しで利用できる
  3. モジュール化
    関連する機能をまとめ、明確なAPIを提供する
  4. 関数型プログラミング
    高階関数やファクトリー関数の作成が可能

実践的な活用場面

  • 設定管理
    アプリケーションの設定値を安全に管理
  • イベントハンドリング
    状態を保持するイベントリスナー
  • キャッシュ機能
    計算結果の記憶による性能向上
  • ライブラリ開発
    名前空間の汚染を防ぐモジュールパターン

注意すべきポイント

  • メモリリーク
    不要な参照は適切にクリアする
  • デバッグの複雑さ
    スコープチェーンが深くなると追跡が困難
  • パフォーマンス
    過度のクロージャ使用は性能に影響する可能性

とくに、レキシカルスコープの理解は、this の挙動やアロー関数と通常の関数の違いを把握するために重要です。

また、モジュールパターンは、ES Moduleが普及する前から使われてきた重要なパターンであり、既存のコードベースでよく見かけるため、理解しておくことが大切です。近年は、ES Moduleやクラスなどのモダンな機能が推奨されますが、クロージャの概念はそれらの基盤の知識となります。

次回は、オブジェクトの基本的な操作からMap/Setなどの新しいデータ構造まで、JavaScriptのオブジェクト操作について紹介します。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?