486
470

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

クロージャ再考

Last updated at Posted at 2015-09-13

はじめに

中級以上のJavaScriptプログラマを目指す上で、避けては通れない壁の一つは クロージャ(Closure) だと思います。「関数の外側で定義された変数を持つ関数の実行時オブジェクトです」とさらっと説明されることが多いですが、シンプルなだけに理解したつもりになって実は使いどころが分からないってことになりがちです。きちんと自分のものにするには基本から丁寧に理解しないとダメですねってことで、今回はクロージャについてその背景から調べてみたいと思います。

歴史と語源

まずは歴史から振り返ってみたいと思います。Wikipediaによるクロージャの解説では、以下のような説明があります。

クロージャの概念は、1960年代にラムダ計算の機械的な実行モデルを構築するために生み出され、1970年に 静的スコープ第一級関数 をサポートするPALプログラミング言語で最初に実装された。1964年、Peter LandinはSECDマシンにおいて 環境とコードのペア として クロージャ を定義した。Joel Mosesは 自由変数 を持つ 開いたラムダ式 がその静的なスコープによって閉じられ(束縛され)、 閉じたラムダ式 となると言う意味において、Landinによりクロージャという用語が導入されたとしている。この用語は、1975年にSussmanとSteeleが発表したScheme(静的スコープを持つLispの一種)においても引き続いて採用され、その後より広く知られるようになった。

どうやら、1960年代には早くもクロージャという概念が発生したようです。Schemeに至る経緯は、Java Puzzlersの著者であるNeal Gafterさんが、クロージャの歴史で以下のように触れています。

1950年代終わり、John McCarthyはLispを発表した。その特徴の一つは、lambdaで表記される値としての関数である。lambdaはラムダ計算を由来とする。Lispは ダイナミックスコープ を持つ言語である。ダイナミックスコープを持つ言語では、変数参照が評価されると、実行環境は コールスタック 上その変数が定義されたスコープまでさかのぼって探索することにより解決される。

ダイナミックスコープはインタープリタや実装が容易であったが、FUNARG問題 と呼ばれる問題がすぐに明らかとなった。

数々のLispの方言が生まれたが、それら全てはダイナミックスコープを持つ言語であった。そんな中、Guy SteeleとGerald Jay Sussmanは簡潔なLisp方言であるSchemeを発表した。

Schemeはラムダ計算同様に レキシカルスコープ を持つ言語である。変数は、その変数を参照するラムダ式が評価される時点でアクティブな静的に取り囲む変数定義に対して束縛される。実装面から言えば、ラムダ式の評価はクロージャを生成する。クロージャとは、ラムダ式内で参照されるがその外側で定義されたすべての変数に対する束縛を保持するオブジェクトとして表現される。このような変数を 自由変数 と言う。クロージャもしくは関数が引数に対して適用(呼び出される)と、クロージャ内の変数束縛はコードに現れる自由変数に意味を与えるために使用される。クロージャという用語は、単に抽象的な言語要素だけではなく実装をも表現しているのである。

以上から、クロージャについて次のようなことが分かります。

  • もともとは関数型言語のコミュニティから発生した
    関数型言語から発生した概念というのはクロージャ以外にもたくさんあります。Javaの1.8で導入されたラムダも、上でも触れられているラムダ計算1を由来としていますし、Javaで有名になったガベージコレクションも最初に実装されたのはLisp(Wikipedia)です。このような技術や概念を勉強するには、関数型言語について勉強してみるのが近道なのかもしれません。
  • 開いた(Open)な関数(ラムダ式)が閉じた(Closed)関数(ラムダ式)となることに由来する 開いたものが閉じるというところからクロージャ(Closure)と言うようです。日本語では閉包と呼ぶらしいです。
  • レキシカルスコープと第一級関数をサポートする言語の実行時のオブジェクトである JavaScriptもこの条件を満たす言語です。クロージャとは主に実装面の要請から生まれたようですね。上記の解説では、SECDマシンでは環境とコードのペア2、Shcemeでは自由変数を保持する関数オブジェクト(実体)とありますね。

では、開いた関数やレキシカルスコープなど、クロージャを理解する上で必要となるこれらの概念についてみていきたいと思います。

閉じた関数と開いた関数

閉じた関数

関数内部で参照する変数が引数とローカル変数のみの関数のことを 閉じた関数(Closed functions) と呼びます。以下のJavaScriptのコードは閉じた関数の例です。

閉じた関数例
function id(x) {
	return x;
}
function sum(x, y) {
	return x + y;
}
function factorial(n) {
	var result = 1; //ローカル変数
	for (i = 1; i <= n; i++) {
		result = result * i;
	}
	return result;
}

関数の引数やローカル変数として宣言されている変数を 束縛変数(Bound variable) と呼びます。束縛変数は関数内部でしか使用されないため、そのすべての出現箇所の名前を変えても関数の意味は変わりません。例えば、上記のid関数について、引数xの名前をyに変えた次の関数は元の関数と等価です。

function id(y) { 
	return y; 
}

開いた関数

関数の外側で宣言された束縛変数以外の変数を 自由変数(Free variable) と呼びます。 開いた関数(Open functions) とは、自由変数を参照する関数です。次のコードは開いた関数の例です。

開いた関数例
function equalsSome(x) {
	return x === some; //someは自由変数
}
function increment() {
	count++; //countは自由変数
}

自由変数は束縛変数のように勝手に名前を変更することはできません。

開いた関数は、その自由変数が定義されていないコンテキストで実行するとエラーとなります。

function increment() {
	count++;
}

increment(); //Error!
console.log(count);

一方、自由変数と同じ名前の変数が定義されている環境であれば、関数呼び出しは成功します。

var count = 0; //自由変数の定義
function increment() {
	count++;
}

increment();
console.log(count); // OK. 1を出力

このように、開いた関数を実行するにはその関数が参照する自由変数の値を取得する必要があります。つまり、開いた関数を実行するには、関数が参照するすべての自由変数の名前とその値へのマッピング情報が実行時に必要ということになります。このようなマッピング情報(変数と値のペアの集合)を 環境 と呼ぶことがあります。

開いた関数が環境から自由変数の値を取り込むことで閉じた関数となるというところがクロージャを理解する上でのキーとなります。

第一級関数と高階関数

第一級関数

数値や文字列値などの は、変数に代入したり、関数の引数や戻り値とすることができます。 第一級関数(first-class functions) とは、このような通常の値と同じように、関数を値として操作できる性質、もしくはそのような値としての関数のことです。Javaは第一級関数をサポートしません 3。Cは関数ポインタを値のように扱えるので同じような性質を持ちますが、普通は第一級関数をサポートするとはみなされません。

JavaScriptの関数はすべて、第一級関数です。したがって、下のように関数リテラルを変数に代入できます。

//関数リテラルを変数に代入
var sum = function (x, y) {
	return x+y;
}

sum(1,2); // 結果は3

また、次のように、関数の引数として関数リテラルを渡したり、関数の戻り値として関数リテラルを返すこともできます。

//引数で受け取った関数を2回適用する関数
function twice(f, n) {
	return f(f(n));
}
//値の平方を求める関数
var square = function(n) {
	return n * n;
}
//関数を引数として渡す
twice(square, 2); //結果は16

//自分自身を返す関数
function returnSelf(n) {
	console.log(n);
	return returnSelf;
}
returnSelf(1)(2)(3); //123と出力される

高階関数

上記のtwicereturnSelf高階関数(higher-order function) と呼ばれます。高階関数とは以下のいずれかの性質を持つ関数のことを指します。

  • 引数として関数を受け取る
  • 戻り値として関数を返す

高階関数を使うと、コードを圧縮して宣言的にプログラミングすることができるようになります。例えば、JavaScriptのArrayオブジェクトはfilterメソッドを持っています。filterメソッドは指定されたフィルタ関数(述語)に一致する要素のみをフィルタリングする高階関数です。以下のコードはfilterメソッドにより偶数のみの要素や奇数のみの要素を抽出するコード例です。

//evensは偶数のみの要素配列[2,4,6,8,10]となる
var evens = [1,2,3,4,5,6,7,8,9,10].filter(function (element) {
	return (element % 2 === 0);
});

//oddsは奇数のみの要素配列[1,3,5,7,9]となる
var odds = [1,2,3,4,5,6,7,8,9,10].filter(function (element) {
	return (element % 2 === 1);
});

高階関数であるfilterメソッドが、配列のループ処理、フィルタ処理、要素の抽出といった制御処理を内部的に行ってくれるため、ユーザはフィルタ関数だけを取り替えることで、多様な処理を実現することが可能になります。

ネストした関数とブロック構造

ネストした関数

ある関数が別の関数の内側で宣言されているとき、この関数を ネストした関数(nested function) と呼びます。ネストした関数は ローカル関数(local function) とも呼ばれます。JavaやCはネストした関数を利用できません。一方、JavaScriptはネストした関数をサポートしています。

次のコードはJavaScriptでのネストした関数の例です。

//配列から指定の値に等しい要素のみを抽出する関数
function filterEqual(array, n) {
	//ネストした関数
	function isEqual(element) {
		return element === n;
	}
	
	return array.filter(isEqual);	
}

filterEqual([1,2,3,4,2,5,2], 2); //結果は[2,2,2]

ネストした関数は、それを囲む関数の外側からはアクセスできないため、グローバルな名前空間を汚さないというメリットがあります。

ネストした関数のもう一つ重要な性質として、外側の関数のローカル変数に自由にアクセスできるという点があります。例えば、上記のコード例のネストした関数であるisEqualは、外側の関数filterEqualsの引数nを直接参照しています。

ブロック構造

ネストした関数をサポートする言語は、 ブロック構造 を持つ言語でもあります。ブロックとは、一連の変数宣言や文のグループです。JavaScirptでは、{}で囲まれた部分が一つのブロックとなります。関数の他に、if文やwhile文がブロックを持ちます。ブロックはいくつでもネストすることができ、階層構造を持つプログラムを構成できます。(ただし、JavaScriptでは、関数は関数ブロックの内部でのみしか宣言できません)

レキシカルスコープとダイナミックスコープ

レキシカルスコープ

ブロック構造を持つプログラミング言語では、ブロック内で宣言された変数は通常そのブロック内部でのみ参照可能です。このような、変数の有効範囲(変数が参照できる範囲)を スコープ と呼びます。JavaScriptでは、 関数の外で宣言された変数はプログラムのどこからでも参照可能な グローバルスコープ を持ち、関数ブロック内部で宣言された変数はその関数内でのみ有効な 関数スコープ となります4

通常はブロックが入れ子になっている場合、外側のブロックで宣言された変数は内側のブロックでも参照可能です。これは、JavaScriptにおいても同じで、下のようにネストした関数から外側の関数(スコープ)の変数を参照することができます。

function outer() {
  var x = "outer"; //outer関数のローカル変数
  
  function inner() {
    console.log(x); //外側の関数outerのローカル変数xを参照できる
  }
  
  inner();
}

outer(); // "outer"が出力される

さて、関数innerにとってxは自由変数でが、関数innerouterの内側で宣言されているため、innerが参照する変数xouterのローカル変数であることは関数宣言をみればすぐに分かります。このように、関数が宣言された場所のスコープから自由変数が静的に決まるとき、このようなスコープ方式を レキシカルスコープ(lexical scope) もしくは 静的スコープ(static scope) と呼びます。

ダイナミックスコープ

JavaScriptのプログラマにとってレキシカルスコープは当然のように思えますが、 ダイナミックスコープ(dynamic scope) と呼ばれるスコープ方式をサポートする言語もあります。ダイナミックスコープの下では、自由変数は関数の宣言時に決まるのではなく、関数の呼び出し時のスコープから動的に決まります。

以下のコードを見てください。innerは自由変数xを持ちます。dynamicは変数xを宣言したのち、inner関数を呼び出します。レキシカルスコープを持つJavaScriptでは、dymamicが宣言する変数xは、innerが参照するxとは別のスコープとなるため、dynamic関数内での変数xの宣言はinnerの実行に影響しません。一方、もし、JavaScriptがダイナミックスコープを持つと仮定するなら、dynamic関数内部でinnerが実行されると、innerが参照する自由変数xdynamicのローカル変数となります。

function outer() {
  var x = "outer";
  
  function inner() {
    // xは自由変数
    console.log(x);
  }

  function dynamic() {
    // 変数xを宣言して、自由変数xを持つinner関数を呼び出す
    // ダイナミックスコープの場合、innerが内部で参照するxは、この変数に実行時解決される。
    var x = "dynamic";
    inner();
  }
  
  dynamic();
}

outer(); // もしJavaScriptがダイナミックスコープをサポートするなら、"dynamic"が出力されるはず。(実際は"outer"と出力される)

ダイナミックスコープは実装が容易(変数の解決はコールスタックを辿ればよい)という利点があり、もともとLispなどで採用されていたようです。ただし、実行時の構造に依存するためバグを生みやすいという点や、レキシカルスコープはより強力なクロージャを利用できるという点などから、現在の代表的な言語はほぼレキシカルスコープを採用しています。

アクティベーションレコードとFUNARG問題

アクティベーションレコード

C言語のような内部関数や高階関数をサポートしない言語では、関数の実行時に アクティベーションレコード と呼ばれる一時領域が生成されます。アクティベーションレコードは、 スタックフレーム とも呼ばれ、関数のローカル変数や関数終了時の戻り先を保持する記憶領域です。関数の呼び出し毎にアクティベーションレコードが生成されて コールスタック と呼ばれるスタック領域にpushされます。関数が終了するとコールスタックからアクティベーションレコードがpopされて呼び出し元の関数数の実行が再開されます。

この実行モデルをJavaScriptのプログラムを例にとって説明してみましょう。以下のサンプルコードを見てください。関数fooはローカル変数x(引数)とyを持ち、関数barを内部的に呼び出しています。関数barはローカル変数x(引数)とzを持っています。

function foo(x) {
	var y = 10; // (a)
	bar(x + y); // (b)
	console.log('foo finished'); //(c)
}

function bar(x) {
	var z = 50;
	console.log(x + z); //(d)
}

foo(10); //(x)

さて、(x)の地点において、fooが呼び出される前のコールスタックは空です。ここで、fooが引数10で呼び出され、地点(a)に来たとき、コールスタックは次のようになります。ここで、f:{a:1,b:2}は、関数fのアクティベーションレコードを表し、値が1のローカル変数aと値が2のローカル変数bを記憶領域として持っているものとします。

コールスタック
foo: {x:10, y:10}

次に、(b)の地点で関数barが引数20(=x+y)呼びだされると、コールスタックはbarのアクティベーションレコードがpushされて以下のようになります。地点(d)でconsole.log関数が参照する変数xzの値は、bar自身のアクティベーションレコードから取得できます。

コールスタック
bar: {x:20, z:50}
foo: {x:10, y:10}

最後に、関数barの実行が終わり、(c)の地点に到達すると、barのアクティベーションレコードはpopされてコールスタックはfooのアクティベーションレコードのみが残ります。

コールスタック
foo: {x:10, y:10}

最後に、fooの実行も終了するとコールスタックは再度空となります。

FUNARG問題

レキシカルスコープや高階関数をサポートするJavaScriptのような言語では、上記の実行モデルでは解決できない問題が発生します。このような問題を一般的に FUNARG問題と呼びます。JavaScriptのコードを用いて、FUNARG問題の具体例を考えてみましょう。

ケース1

例えば、以下のJavaScriptのコードをみてください。barfooのローカル変数xを出力する単純な関数です。

function foo(x) {
	function bar() {
		console.log(x); //(b)
	}
	
	bar(); //(a)
}

foo(10); //10が出力される

地点(a)でのbarの呼び出し前のコールスタックは次のようになるでしょう。(ここで、JavaScriptでは関数も第一級の値であるので、アクティベーションレコードにその関数の実体が保持されます。)

コールスタック
foo: {x:10, bar:barの関数実体}

さて、barが呼び出されて地点(b)に到達したときのコールスタックは次のようになります。barはローカル変数を持たないため、アクティベーションレコードも空です。

コールスタック
bar: {}
foo: {x:10, bar:barの関数実体}

ここで、console.log(x)が実行されたとき、アクティベーションレコードは空であるため、変数xの値を取得することができません。

上記のケースでは、fooのアクティベーションレコードから変数xの値を取得できるのでは?と思われるかもしれませんが、これはダイナミックスコープをサポートする言語において有効な手法です。しかし、レキシカルスコープを採用するJavaScriptでは解決できません。これが、ダイナミックスコープの実装が容易である理由の一つです。(本記事のレキシカルスコープとダイナミックスコープの解説を参照してみてください。)

ケース2

さらに、以下のようなコードを考えてみましょう。funarg関数は内部関数fooを持ちます。fooは関数barをリターンする高階関数です。funargを実行すると3が出力されます。

function funarg() {
	function foo(x) {
		function bar() {
			console.log(x); //(c)
		}
	
		return bar;
	}

	var f = foo(3); //(a)
	f();//3が出力される //(b)
}

funarg(); //(x)

地点(x)でfunargを実行すると、コールスタックは以下のようになります。ここで、ローカル変数fの値は、funargの実行開始時にはまだ決まらないためundefinedとなります。

コールスタック
funarg: {foo:関数の実体, f:undefined}

地点(a)でfooが引数3で実行されたとき、コールスタックにfooのアクティベーションレコードがpushされます。

コールスタック
foo: {x:3, bar:bar関数の実体(*1)}
funarg: {foo:関数の実体, f:undefined}

関数fooの実行が終わると、fooのリターン値であるbar関数の実体(*1)が変数fに代入されます。この時点で、コールスタックからはfooのアクティベーションレコードはpopされます。

コールスタック
funarg: {foo:関数の実体, f:bar関数の実体(*1)}

さて、地点(b)において f(bar関数)の実行が開始されると、barのアクティベーションレコードがpushされてコールスタックは次のようになります。

コールスタック
bar: {}
funarg: {foo:関数の実体, f:bar関数の実体(*1)}

さて、ここでbarのコードが実行されて、地点(c)に到達したとします。このとき、変数xの値はコールスタック上のどこにも存在しません。よって、barを実行することはできません。

クロージャ

JavaScriptはレキシカルスコープと内部関数(と高階関数)をサポートする言語です。上記でみたように、コールスタックを用いたシンプルな実行モデルではFUNARG問題が発生します。どうやってこの問題を解決できるでしょうか?

次のコードをみてください。関数barは外側の関数fooのローカル変数xを自由変数として参照する開いた関数です。barのような開いた関数を実行するには、実行時に自由変数の値を取得する方法が必要になります。これを実現するには、barの実行時に、(xの値を保持する)fooのアクティベーションレコードを参照できればよさそうです。

function foo(x) {
	function bar(y) {
		console.log(x + y); // (z) xは自由変数。
	}
	
	bar(1); // (b) barの実行時、barが参照する自由変数xの値は1 
	bar(2); // (c) barの実行時、barが参照する自由変数xの値は2 
	bar(10); // (d) barの実行時、barが参照する自由変数xの値は10
}

foo(100); // (a) fooを実行。101, 102, 110が出力される。
foo(200); // (e) fooを実行。201, 202, 210が出力される。

JavaScriptの実行エンジンは、関数宣言を評価すると、関数自身のコードとその時点でアクティブなアクティベーションレコード(への参照)を保持するオブジェクトを生成します。これが、 クロージャ の正体です。関数の実行はクロージャを実行することであり、クロージャが実行されると、その関数自身のローカル変数を保持する自分自身のアクティベーションレコードが生成されるとともに、クロージャが参照するアクティベーションレコードへのリンクが生成されます。このリンクを辿っていくことで自由変数の値を参照できるようになります。

アクティベーションレコードは他のアクティベーションレコードから参照(リンク)される可能性があるため、コールスタックモデルのように関数呼び出しが終了した時点でアクティベーションレコードを削除(pop)することはできません。そのため、アクティベーションレコードはスタックではなく、ヒープなどのデータ領域に生成されます。そして、不要になった時点でGCなどが削除します。(この辺は各実行環境の実装によるようですが、以下の説明ではヒープとします。)

実行モデル

以下では、f:{x:1, y:2, _scope_:n}を関数fのアクティベーションレコード(xyfのローカル変数とその値)とします。ここで、_scope_はアドレスnで参照される別のアクティベーションレコードを表します。また、Closure(f, n)は関数fとアドレスnで参照されるアクティベーションレコードから成るクロージャとします。

さて、上記のサンプルコードに戻りましょう。地点(a)でfoo(100)が実行されると、JavaScriptの実行エンジンは以下のような動作を行います。

  1. ローカル変数xの値を100で初期化したアクティベーションレコードfoo{x:100, bar:undefined}をヒープに生成する。
アドレス ヒープ
ref(1) foo:{x:100, bar:undefined}
 
  1. barの関数宣言を評価し、barと1.で生成したfooのアクティベーションレコードから成るクロージャClosure(bar, ref(1))を生成する。この時点で、ヒープ領域は次のようになる。
アドレス ヒープ
ref(1) foo:{x:100, bar:Closure(bar, ref(1))}
 
  1. 地点(b)において、2.で生成したクロージャClosure(bar, ref(1))を引数で呼び出す。このとき、barのアクティベーションレコードbar{y:1, _scope_:ref(1)}が生成される。ここで、_scope_にはクロージャの参照アドレスが設定されることに注目。
アドレス ヒープ
ref(2) bar:{y:1, _scope_:ref(1)}
ref(1) foo:{x:100, bar:Closure(bar, ref(1))}
 
  1. barの実行を開始する。
  2. 地点(z)において、console.log(x+y)を実行する。
    ここで、変数yの値はref(2)で表される自身のアクティベーションレコードから取得できる。自由変数xの値は、_scope_を辿ることでfooのアクティベーションレコードから取得できる。
  3. 地点(c)において、bar(2)の実行を開始する。(1.で生成したクロージャClosure(bar, ref(1))を引数2で呼び出す)。3〜5を繰り返す。地点(z)におけるヒープの状態は次のようになる。
アドレス ヒープ
ref(3) bar:{y:2, _scope_:ref(1)}
ref(1) foo:{x:100, bar:Closure(bar, ref(1))}
 
  1. 地点(d)において、bar(10)の実行を開始する。(1.で生成したクロージャClosure(bar, ref(1))を引数10で呼び出す)。3〜5を繰り返す。地点(z)におけるヒープの状態は次のようになる。
アドレス ヒープ
ref(4) bar:{y:100, _scope_:ref(1)}
ref(1) foo:{x:100, bar:Closure(bar, ref(1))}

ここで、fooの呼び出しごとに、fooの異なるアクティベーションレコードが生成されることに注意してください。つまり、foo(100)foo(200)の呼び出しごとにその内部のbar関数が参照する自由変数xの実体も異なります。

自由変数の共有

もう一つ重要な点は、JavaScriptでは、クロージャが参照する自由変数は値のコピーではなく変数そのものであることです。これにより、変数の値を書き換えることが可能であり、同じ関数内部のクロージャは同一の変数を変数を共有できます。次のコードでは、ネストした関数barupdateは同一の変数xを参照しています。update関数のように、自由変数xの値を更新できることが分かります。

function foo(x) {
	function bar() {
		console.log(x);
	}
	
	function update(y) {
		x = y;
	}
	
	bar(); //100が出力される
	update(10);
	bar(); //10が出力される
}

foo(100);

関数より長生きするローカル変数

関数を返す高階関数とレキシカルスコープを組み合わせると、さらに面白いことが実現できます。次の関数をみてください。この関数は、引数として2つの関数fおよびgを受け取り、fgを合成した関数を返す高階関数です。すなわち、compose(f,g)(x)f(g(x))と等価です。

function compose(f, g) {
  return function(x) {
  	//fとgは自由変数
    return f(g(x));
  };
}

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

var f = compose(squre, squre); // (a)
f(2); //結果は16

上記のコードにおいて、関数composeはネストした無名関数を返します。この無名関数は2つの自由変数fgを参照します。これらの自由変数はcompose関数のローカル変数(引数)です。地点(a)において、composeを実行すると、無名関数のクロージャが生成されて返されます。このクロージャはcomposeのアクティベーションレコード(ローカル変数fgを持つ)を保持します。これにより、composeの実行が終了した後でも、クロージャは変数fgを参照することができます。つまり、compose関数の実行が終了したにもかかわらず、そのローカル変数fgは生き続けます。

ネストした関数のセクションで用いたサンプルコードのtwice関数は、compose関数を使うと次のように定義できます。

//引数の関数を2回適用する関数
var twice = function(f, x) {
  return compose(f, f)(x);
};

//引数にプラス1する関数
var plus1 = function(x) {
  return x + 1;
};

console.log(twice(plus1, 1)); //結果は3
console.log(twice(plus1, 2)); //結果は4

また、配列の要素のうち、偶数の要素数をカウントする関数は次のように記述できます。

//配列の偶数の要素数をカウントする関数
var countEven = function(array) {  
  function even(array) {
    return array.filter(function (e) {
      return (e % 2 === 0);
    });
  }
  
  function count(array) {
    return array.length;
  }
 
  return compose(count, even)(array);
};

console.log(countEven([1,2,3,4,5])); //結果は2
console.log(countEven([1,2,3,4,5,6,7,8,9])); //結果は4

クロージャの主な使いどころ

高階関数の引数として自由変数を持つ関数を渡す

以下のコードを見てください。配列オブジェクトの高階関数filterの引数である無名関数は、xを自由変数として持ちます。xfilter関数の外部で宣言された変数です。このように、クロージャを利用することにより、関数(この例ではfilter)の実行時にその外部で宣言された変数を利用できるようになります。5

//配列arrayからxに等しい要素だけを抽出する関数
function filterEquals(array, x) {
	return array.filter(function (element) {
		return element === x;
	});
}

var filtered = filterEquals([1,2,3,4,2], 2); //結果は[2,2]

状態を持つオブジェクトの生成

クロージャは自由変数(データ)と関数(コード)を持ったオブジェクトとみなすことで、オブジェクト指向言語におけるオブジェクトを実現することができます。以下のコードはクロージャの例としてお馴染みのカウンターですが、makeCounter関数によりcount変数をデータとして持つカウンタオブジェクトを生成しています。

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

var counter = makeCounter();
counter();
counter();
console.log(counter()); //2が出力される

以下のように、複数の関数(メソッド)もつオブジェクトも実現できます。

function makeCounter() {
	var count = 0;
	return {
		increment: function() {
			count++;
		},
		decrement: function() {
			count--;
		},
		val: function() {
			return count;
		}
	};
}

var counter = makeCounter();

counter.increment();
counter.increment();
console.log(counter.val()); //2が出力される
counter.decrement();
console.log(counter.val()); //1が出力される

モジュールパターン

クロージャを利用することにより、プログラムをモジュール単位に構造化し、各モジュールの内部構造を外部から隠蔽することができるようになります。よくあるパターンとして、以下のように即時実行関数を実行することでモジュール(オブジェクト)を生成し、公開する関数のみをモジュールに追加する方法です。

//モジュール
var Module = {};

(function(M) {
	//プライベートなローカル変数
	var localVar = "local";
	//プライベートなローカル関数
	function localFunction() {
		return localVar;
	}
	//パブリックな関数はモジュールに追加
	M.globalFunction = function() {
		console.log(localFunction());
	};
  
})(Module); //即時実行関数にモジュールオブジェクトを渡して実行

//パブリック関数の呼び出し
Module.globalFunction(); //localと出力される
//プライベート関数は呼び出せない
Module.localFunction(); //エラー

参考サイトと書籍


  1. アロンゾ・チャーチによって考案された計算モデルです。(Wikipedia

  2. Sはスタック、Eは環境、Cはコード、Dはダンプを表すようです。(Wikipedia

  3. ラムダはオブジェクトであって関数ではありません。

  4. ECMAScript 6ではletによるブロックスコープが導入されています。(MDNによる解説記事)

  5. クロージャを持たないC言語では、ポインタを利用して関数宣言の外部の変数値を受け渡すことができます。

486
470
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
486
470

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?