JavaScriptにおけるクロージャの理解と実装例

変数のスコープとライフサイクル

JavaScriptでは、varキーワードを使用せずに宣言された変数はグローバルスコープに属します。一方、関数内でvarを使用して宣言された変数はローカル変数として扱われ、関数の外からはアクセスできません。ただし、関数内部からはスコープチェーンを介してグローバルオブジェクトまで変数を検索できます。


var outerFunc = function() {
  var localVar = 10;
  console.log(localVar); // 10
};
outerFunc();
console.log(localVar); // エラー: localVar is not defined

クロージャの基本と特徴

通常、関数が終了するとそのスコープ内に存在する変数は破棄されますが、クロージャはこのルールに例外です。クロージャは関数とその関数が生成されたときのレキシカルスコープの組み合わせであり、関数の外からでも内部の変数にアクセスし続けることが可能です。


var counterGenerator = function() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
};

const counterA = counterGenerator();
console.log(counterA()); // 1
console.log(counterA()); // 2

const counterB = counterGenerator();
console.log(counterB()); // 1
console.log(counterB()); // 2

DOMイベントとクロージャの応用

以下の例では、すべてのdiv要素にクリックイベントを割り当てていますが、どの要素をクリックしても同じ値が表示されます。


<div>0</div>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>

<script>
  const elements = document.getElementsByTagName('div');
  for (let index = 0; index < elements.length; index++) {
    elements[index].addEventListener('click', function() {
      alert(index);
    });
  }
</script>

問題の原因は、イベントハンドラが非同期で実行されるため、ループが終了した時点のindex値(最終的に5)がすべてのイベントハンドラで参照されることです。これを解決するには、クロージャを使って各ループの値を保持します。


<script>
  const elements = document.getElementsByTagName('div');
  for (let index = 0; index < elements.length; index++) {
    elements[index].addEventListener('click', (function(value) {
      return function() {
        alert(value);
      };
    })(index));
  }
</script>

配列へのクロージャの代入

次の例では、関数が配列に複数の関数を格納して返しますが、すべての関数が最終的な値を返してしまいます。


function createCounterList() {
  const list = [];
  for (let i = 0; i < 10; i++) {
    list[i] = function() {
      return i;
    };
  }
  return list;
}

for (let j = 0; j < 10; j++) {
  console.log(createCounterList()[j]()); // 全て10
}

これはクロージャが変数iへの参照を保持しているためです。解決策として、即時関数を使って現在の値を固定します。


function createCounterList() {
  const list = [];
  for (let i = 0; i < 10; i++) {
    list[i] = (function(currentValue) {
      return function() {
        return currentValue;
      };
    })(i);
  }
  return list;
}

for (let j = 0; j < 10; j++) {
  console.log(createCounterList()[j]()); // 0から9まで順に
}

クロージャの利点と注意点

クロージャは外部関数の変数にアクセスできるため、関数の内部状態を外部に公開する手段として有効です。しかし、メモリ消費が大きいため、不必要なクロージャの生成は避けるべきです。


function createPerson() {
  let name = 'Alice';
  let id = 16;
  return {
    getInfo: function() {
      return `${name}: ${id}`;
    }
  };
}

const person = createPerson();
console.log(person.getInfo()); // Alice: 16

メモリリークの防止

クロージャ内にDOM要素を保持すると、メモリリークの原因になることがあります。この問題を回避するには、不要な参照を明示的に解除します。


function assignClickHandler() {
  const element = document.getElementById('target');
  const id = element.id;
  element.addEventListener('click', function() {
    console.log(id);
  });
  element = null; // DOM参照を明示的に解除
}

ブロックスコープの模倣

JavaScriptにはブロックスコープが存在しないため、関数スコープを利用してブロックスコープを模倣できます。


(function() {
  for (var i = 0; i < 5; i++) {
    console.log(i); // 0, 1, 2, 3, 4
  }
})();
console.log(i); // エラー: i is not defined

タグ: javascript クロージャ 関数スコープ DOM操作 メモリ管理

7月6日 21:45 投稿