Hatena::ブログ(Diary)

主にプログラムを勉強するブログ

2012-01-15

私が今までクロージャを理解できなかった理由

Javascriptを勉強する上で誰もがつまずくと言われるクロージャですが私も例に漏れず理解できないでいました。
さまざまな解説サイトを読んだりサンプルコードを書き換えてみたりして

  • 静的なスコープの言語で利用できる。
  • 関数が終了したあともそのローカル変数が参照できる。

といったクロージャの「仕組み」や「特徴」については分かったものの、もっとも重要ともいえる
「どういう時にクロージャを使えばいいのか」
が分かりませんでした。言いかえると友人がなんて言って悩んでいる時に
「そう言う時はクロージャを使うといいよ」
と言ってあげればいいのか。

例えばプログラムの勉強を始めた友人が
「これと同じ処理もう何回も書いてるんだよ。コピペばっかりしてる気がする」
と言って悩んでいたら
「そこを関数にすればいいんじゃない?」
って教えてあげますよね。

これと同じように友人が
「○○○○○○なんだよ、うまい方法ない?」
と質問して来た時に
「そういう時はクロージャを使えばいいよ」
と答えてあげるべき○○○○○○は何なのか。

これが分かって初めて自分でもクロージャが使えるはずです。

最近になってようやく私もこのクロージャをどういう時に使うべきかが分かってきました。(と自分では思ってる)
それに伴い「なぜ今まで分からなかったか」という理由も分かってきたので整理したいと思います。
結論から言えば
「(クロージャを使うことで)関数終了後もそのローカル変数を参照できることから得られる2つの大きなメリットの内、一方を分かっていなかったから」
ではないかと考えています。

関数終了後もローカル変数を参照できることのメリット1、グローバル変数の節減


解説書などでよく示されているクロージャのサンプルが「自身が呼ばれた回数をカウントする関数」です。
はてなのクロージャとは - はてなキーワードでも
builderのJavaScriptクロージャを完全理解!スコープチェインを知る(後編) - page2 - builder by ZDNet Japanでも
ほぼ同じサンプルが示されています。

///コード1 (下のアドレスのページより引用)
// 関数オブジェクトをリターンする関数
function createCounter() {
  var n = 0;
  // クロージャを作成して返す
  return function() {
    return n++;
  };
}
// createCounter()を呼び出し、
// 戻り値 (関数オブジェクト) を変数に格納しておく
var counter = createCounter();

alert(counter()); // 0が表示される
alert(counter()); // 1が表示される
...
http://builder.japan.zdnet.com/script/sp_javascript-kickstart-2007/20378258/2/

同じ機能をクロージャを使わずに書くと下のようになるでしょうか。

///コード2
var count = 0;
			
var counter = function(){
	return count++;
}
			
alert(counter()); //0が表示される
alert(counter()); //1が表示される

両者の違いは何かと言うと「使用しなければならないグローバル変数の数」です。上の引用の例ではcreatCounterという名前とcounterという名前の2つが使われていますがcreatCounterの方は無名関数化して

///コード3
var counter = (function(){
	var n = 0;
  	// クロージャを作成して返す
  	return function() {
   		return n++;
 	};
})();

alert(counter()); // 0が表示される
alert(counter()); // 1が表示される

と、書くこともできるためどうしても使用が避けられないグローバル変数の数は実質1つです。
これに対してコード2の例ではcountという変数とcounterという変数はどちらも必ず使わざるを得ません。counterはもちろんのことcountの方も肝心の呼ばれた回数を保存しているからです。
そもそもなぜグローバル変数が少ない方がいいかという話は本題からそれるので避けますがクロージャのメリットの1つがこのことである事はまちがいありません。
そしてここまではクロージャを説明するサンプルコードでもまず間違いなくしめされています。

さてここからが本題ということになります。(ようやく(^_^;))

関数終了後もローカル変数を参照できることのメリット2、クロージャを使うと1のメリットを享受しつつ計算量を増やさないことができる


さて、上の「自身が呼ばれた回数をカウントする関数」ではクロージャを使うことでグローバル変数を減らすことができましたが、一方で計算量そのものは変化がありませんでした。グローバル変数かローカル変数かの差はあれどcounter()が実行されるたびにある変数を+1していくという処理に違いはないからです。
クロージャの「関数終了後もローカル変数を参照できるという特徴」を使うと次のように普通に書くと計算量が増えてしまう場合にもそれを防ぐことができます。

今、lang という変数の値が"English"であれば"Hello"を"Japanese"であれば"こんにちは"という文字列をアラートするhello()という関数を考えてみます。
全く何も考えずに実装すれば次のようになります。

///コード4
var lang = "Japanese"; //ここではベタ書きですがブラウザの言語設定をみて値を入れるなどするかもしれません
var text; //ここに"Hello"または"こんにちは"を入れる
			
if(lang == "English"){
	text = "Hello";
} else if(lang == "Japanese"){
	text = "こんにちは";
}
			
function hello(){
	window.alert(text);
}
	
hello();

lang,text,helloと3つもグローバル変数を使ってしまっています。langとhelloは無理ですがtextはいかにもローカル変数で代用できそうです。
クロージャの出番か!?となるわけですがtextをローカル変数にするだけであればクロージャを使わなくても次のように書けば良いだけです。

///コード5
var lang = "Japanese";
			
function hello(){
	var text;
	if (lang == "English") {
		text = "Hello";
	} else if (lang == "Japanese") {
		text = "こんにちは";
	}
	window.alert(text);
}
			
hello();

見事にグローバル変数はlangとhelloのみになりました。しかしhello関数の内側に注目してください。
このhello関数はtextをローカル変数にしたために関数が呼ばれるたびにlangの値が"English"なのか"Japanese"なのかを判別しなくてはならなくなってしまいました。
textがグローバル変数だったコード4ではlangの中身を確認するifの処理は一度行うだけで良かったのにグローバル変数を減らした代償としてhello関数が呼ばれるたびに毎回毎回ifの処理をしなくてはならなくなったわけです。

うーん、なんとかグローバル変数の数を減らし、しかもifの処理が一度ですむ方法はないものか。
そうです。あります。それこそがクロージャの真価です。

///コード6
var lang = "English";
			
var hello = (function(){
	var text;
	if (lang == "English") {
		text = "Hello";
	} else if (lang == "Japanese") {
		text = "こんにちは";
	}
				
	return function(){
		window.alert(text);
	};
})();
			
hello();

できました!クロージャの書き方自体はコード3の無名関数を使った例と同じです。
コード6でhello関数の中身に実際に入っているのは

function(){
	window.alert(text);
}

という内容だけですがこのtextはローカル変数のtextなので一度ifで"Hello"または"こんにちは"が代入された後はずっと同じ値が入っています。
クロージャの「関数終了後もローカル変数を参照できる特徴」によって見事グローバル変数を減らすことと、処理を増やさないことを両立したわけです。

まとめ

さて上でもチラッと書いたようにクロージャに関して説明しているページや本は数あれどその多くは1の特徴のみを説明する場合が多く、クロージャの真価でもある1と2の両立が説明されている所があまりないように思います。カウンターの例では「グローバル変数を減らした変わりに計算量が増えてしまった」コード5のパターンが想像しづらいため2の特徴に気づかない、とも言えるかもしれません。
私の場合このことがいまいちクロージャの使いどころが分からなかった原因のように感じます。

一番最初に書いた、友人がなんて言って悩んでいる時に
「そう言う時はクロージャを使うといいよ」
と言ってあげればいいのか。

これの答えは
「しょっちゅう呼び出す関数なんだけどその処理の内の一部は呼ばれるたびに全く同じことをやるだけなんだよね。かといってその一部をグローバルに置くのは困るんだ。うまい方法ない?」
という感じになるのではないでしょうか。

※念のため断りを入れておきますが上でリンクしているサイトの例が悪いと言っているのではありません。これらは紛れもなく「最も簡単なクロージャの例」であり、何かを説明する時に「最も簡単な例」を使うことは至極当然のことだからです。

以上が私なりのクロージャに関する整理でした。もし読んでいただいた方の理解の一助となればこれほどうれしいことはありません。

manji6manji6 2012/10/15 01:44 はじめまして。
クロージャーの認識自体はあってると思うんですが、利用用途はもうちょっとあるかな・・・?って気がします。
「グローバル変数定義を減らす」はOKだとして、「計算量を減らす」はちょっと極論すぎるかも。

ポイントは「クロージャーは引数以外の変数を、定義したその時に設定されたものを利用すると保証できている」ところじゃないですかね。
だから計算量も減らせる(=クロージャー定義時に計算した結果を格納するため)とは思うんですが。

ここの例が一番参考になるかもです。非同期通信みたいに実行した時のデータを元に非同期通信のコールバック処理をしてもらいたい場合は使えますね。
http://techtipshoge.blogspot.jp/2011/06/blog-post_10.html

artgearartgear 2012/10/15 15:32 コメントありがとうございます。
ご指摘の通りだと思います。これを書いた当時は広範な用途については私自身が理解できていなかったのと、具体例を重視して書いたので上のような内容になったと思います(^_^;)

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証