【JS】 クロージャーについて

【JS】 クロージャーについて

前提知識

クロージャーを知る上で、まずはJavascriptの「スコープ」について知る必要があります。

スコープとは変数の名前や関数などの参照できる範囲を決めるものです。 スコープの中で定義された変数はスコープの内側でのみ参照でき、スコープの外側からは参照できません。

jsPrimer

以下、スコープに関する参考記事です。

関数とスコープ – JavaScript Primer

クロージャーとは?

クロージャーとは、親スコープの変数を関数が使用している状態。
また、「外側のスコープにある変数への参照を保持できる」という関数の性質を指します。

主な役割

クロージャーの主な使い道としては以下の2つが挙げられます。

  • グローバル変数の使用回避、プライベート変数の定義
  • 関数に状態を持たせる
  • 高階関数の一部としての機能

プライベート変数の定義

クロージャーを使用することで、グローバルコンテキストに変数を定義することなく関数内の状態を管理できます。

具体例を以下に示します。

function fn() {
	let name = '太郎';// name は、fn()内のプライベート変数
	function displayName() {// 内部関数
		console.log(name);
	}
	displayName();
}

fn();// 太郎
  • 変数namefn()内のプライベート変数で外部から参照されない
  • displayName()は fn() の中で使用する内部関数
  • displayName()は親スコープにある変数nameを参照し、使用できる

グローバルコンテキストに変数を定義する場合、変数の値が外部から参照されてしまうため意図しない挙動をとることがありますが、
外部スコープにある変数への参照を保持できるという性質を使って、内部関数で処理を実行することで外部から参照されないプライベート変数の定義が可能になります。

関数に状態を持たせる

プライベート変数の定義に加えて、クロージャーにより関数内から特定の変数を参照し続けることで関数が状態を持つことができます。

// 呼び出すたびにカウントアップした数値を出力する関数
function createCounter() {
	let num = 0;

	return function increment() {
		num++;
		console.log(num);
	};
}

const counter = createCounter();
counter();// => 1
counter();// => 2
counter();// => 3

const newCounter = createCounter();// 初期化
newCounter();// => 1
newCounter();// => 2
newCounter();// => 3

この時、countercreateCounter()の実行時に戻り値であるincrement()のインスタンスを保持、
increment()は親スコープにある変数numの参照を保持しており、
counterincrement()を経由して変数numへの参照が残っているので、実行時にnum++の処理が走ります。

createCounter()が実行された後でも変数numcounterから参照され続けているので、メモリが開放されずに状態を保つことができます。

以降、親関数であるcreateCounter()が実行された時はそれぞれ別の変数numincrement()が定義され、newCounterからは別の変数numが参照されるので新たなカウンターが生成されます。

変数の値が保持される理由については参照されなくなったデータはガベージコレクションにより解放されるというJavaScriptのメモリ管理の仕組みが関わってきます。
メモリ管理に関しては以下の記事を参考にしました。

高階関数の一部として機能する

クロージャーで定義することで、先ほどの例のようにグローバルな変数を持つことなく関数内の状態を管理できることに加えて、

以下のように高階関数の一部として利用することもできます。
ちなみに、関数の引数や戻り値に関数を利用した関数のことを「高階関数」と呼びます。

function addNumber(num) {
  return function (value) {
    return  num + value;
  }
}

const add5 = addNumber(5);// addNumberの引数に5を代入した状態の関数add5定義
const add10 = addNumber(10);// addNumberの引数に10を代入した状態の関数add10定義

console.log(add5(10));// 5 + 10 で 15と出力される
console.log(add10(10));// 10 + 10 で 20と出力される

上の例ではadd5add10addNumberの戻り値に定義した関数を共有していることから、
addNumber()に渡す引数によって動的に関数を定義することができます。

複数の関数を返す場合

複数の関数を返す例として四則演算を行うメソッドを持ったオブジェクトをクロージャーを用いて作成します。
四則演算を行うメソッド(plus, minus, multiply, divide)を実行すると計算結果が出力されます。

function calcFactory(num) {
  return {
    plus: (value) => {
      let result = num + value;
      console.log(`${num} + ${value} = ${result}`);
      num = result;// numにresultを格納する
    },
    minus: (value) => {
      let result = num - value;
      console.log(`${num} - ${value} = ${result}`);
      num = result;
    },
    multiply: (value) => {
      let result = num * value;
      console.log(`${num} * ${value} = ${result}`);
      num = result;
    },
    divide: (value) => {
      let result = num / value;
      console.log(`${num} / ${value} = ${result}`);
      num = result;
    },
  }
}
const calc = calcFactory(10);// 初期値を10として設定
calc.plus(5);// => 10 + 5 = 15
calc.minus(3);// => 15 - 3 = 12
calc.multiply(3);// => 12 * 3 = 36
calc.divide(2);// => 36 / 2 = 18

戻り値のメソッドはそれぞれ関数スコープを持っているため変数resultは通常、外部から参照されませんが、この例ではメソッドを呼ぶたびに前回の計算結果をもとに四則演算を行いたいために、計算結果である変数resultをレキシカルスコープにあたる関数の引数numに格納しています。

クロージャーを実用してみる

実際に使いそうな機能をクロージャーを用いて作成してみました。

名簿リスト

  • 値が保持されるため関数を呼び出すたびにナンバリングされる
createPerson("太郎", 25, "男");

このように引数にそれぞれ任意の値を入れることでリストが生成されます。

クリック状態の保持

  • フォームなどのリクエストの重複を制御する

まとめ: クロージャーを使用する場面

クロージャー使用する場面には以下が考えられます

  • 外から参照できない変数を定義する手段として
  • グローバル変数を減らす手段として
  • 関数に状態を持たせたい時

逆にこれらを必要としない場面で無闇に使わないように注意したいです。

以下MDN – クロージャから引用

あるタスクを実行する時、クロージャが必要とされていないのにいたずらに関数を他の関数の中に作成するのは、スクリプトのパフォーマンスに悪影響を及ぼすのであまり賢いやり方ではありません。

参考文献