ここ最近、デコレータとかクロージャなどの話題が続いていたが
そもそもクロージャとは何なのだ。
wikipediaでクロージャを調べてもチンプンカンプンだろう。
これはある程度クロージャをしてている人が読んで、なるほどって思える文章だ。
本当に必要な人には届かない。
※前回の記事たち
Pythonでデコレータの引数を元に関数を動的に呼び出すアレについて。
クロージャを使ったデコレータをJavaScriptでも自前実装
関数は変数として扱える
JavaScriptとかPythonなど、多くの言語は関数を変数に入れたり、引数に渡したり、戻り値として扱える。
Javaにはそういう機能はないので、Java出身者にとってはここらへんで結構混乱するかと思う。
下記のコードは、JavaScriptで書いてみた。
Pythonよりも開発者人口が多いのでJavaScriptにしてみたが、
JavaScriptを僕はあまり真剣にやってきていないので、少々拙いかもしれないのだが、そこはご容赦いただきたい。
JavaScriptの関数の定義方法はいくつかあるが、var func = function(){}; みたいな書き方も良くする書き方の一つだ。
この書き方を見てみると、まさに変数に代入しているということがわかる。
func2には引数でfunc1を渡している。
func2内で作られたfunc3が戻り値として帰っている。
こういう書き方がJavaScriptを始め多くのスクリプトではよくやる。
関数を関数に渡すという書き方を良くするのは、ある処理の前後の処理を渡すということ。
処理ごとに細かく関数を分けて組み合わせることができるので、便利だ。
関数が返ってくる関数は後ほど実践する。
(function(){ var func1 = function(){ console.log("func1だっよ"); }; var func2 = function(fn){ fn(); var func3 = function(){ console.log("func3だぜ"); } return func3; }; var fn3 = func2(func1); fn3(); })();
スコープのこと
スコープというのは、変数を宣言したらその変数が有効な範囲のこと。
関数内で宣言した変数は、関数を越えることはできない。
戻り値で返すことはできる。
言語によってはif内でもスコープが区切られるものもあるが、JavaScriptではスコープの区切りは関数になっている。
とりあえず、関数内で宣言した変数はその関数の中でしか生きられないし、
関数の処理が終われば消えてしまう。
その関数がまた実行された時は、新たに変数が宣言されるだけ。
関数の外で宣言した変数は、その変数が終わっても生き残る。
変数が変わったら、次回関数を実行したときに変わった状態の変数が参照される。
(function(){ var funcA = function(){ var a = "aだよ"; }; funcA(); /* ここで参照はできない */ // console.log(a); //undefinedになる var b = "bだよ"; var funcB = function(){ console.log(b); //bだよ と出力 }; funcB(); var b = "bだよze"; funcB(); })();
クロージャの書き方
クロージャというのはこの2つの知識を組み合わせたものだ。
関数を戻り値で返す関数 + 関数の外で宣言された変数は関数と共に終了しない。
単刀直入に基本的な書き方はこうだ。
- 関数をreturnする関数を書く。
- returnされる関数はその関数の外で宣言された変数を参照している
- 関数をreturnする関数を実行して、中の関数を取得する。
f1とf2はそれぞれ同じfunc関数を実行して、func内のfunctmp関数を取得しているが、
それぞれで実行されているので、functmp関数が参照している変数iは別物を見ていることになっている。
functmp内で変数iはインクリメントしており、f1が実行するたびにカウントアップされる。
f2を実行するとf1とは別物の変数iがカウントアップされる。
このパターンで作られた関数は、普通の関数とは違って
状態が保存されている。(変数iのカウントアップされている情報をずっと持っている)
そして、生成された関数は生成されたごとに独立した状態を持っている。
(f1とf2は同じ関数なのだが、カウントアップの数値は別々になっている)
(function(){ var func = function() { var i = 0; var functmp = function(){ console.log(i); i++; }; return functmp; }; var f1 = func(); var f2 = func(); f1(); //0と表示 f1(); // 1と表示 f2(); // 0と表示 f2(); // 1と表示 f1(); // 2と表示 })();
クロージャの使いどころ
Pythonではこの仕組みをつかって、デコレータという機能を実現している。
デコレータ自体は関数をreturnする関数になっている。
詳しくは前回の記事を見てほしい。
デコレータ以外には、先程の状態が保存される関数という性質を使って、
実行回数の偶数回目と奇数回目で処理が入れ替わるような処理とかにも使える。それほどこの方式に出番はないけれど…
この状態を保つ機能を使って、配列の中身を関数が呼ばれたタイミングで順繰りに出していくというものも作れる。
下記ソース参照
(function(){ var func1 = function(ary){ var i = 0; return function(){ if(ary.length <= i) { return null; } var result = ary[i]; i++; return result; }; }; var f1 = func1([1,2,3,4,5,6,7,8,9,0]); var num = f1(); while(num !== null){ console.log(num); num = f1(); } })();
これはPythonではyieldという機能としてある。
forでいいじゃんって思われるかもしれないけれども、
forだと配列の全要素をまとめて計算していくことになるけれども、それだと計算量が多くなって遅くなってしまう場合がある。
今回は順繰りに値を返すだけだけれども、まだまだ利点がある。
(yieldの利点だけれども)
func1の引数にもう一個関数を増やして、何か配列の要素を変化させる関数とかを入れておく。
func1から作られたf1関数を一回実行すれば、その都度渡した関数が実行され
処理した内容を受け取れる。
これはfor文でまとめて配列を書き換えるより、必要な分だけ計算できる。
「いやいや、forの回数決めてー、必要な回数まわせばいいじゃないのー、そして配列新しく作ればいいじゃないのー」というのももっともなのだが、
その都度別の配列を作ったり、処理の途中に作られる副産物が多くなる。
だったら一つの関数内に押し込めて、
呼び出したときに書き換わるだけのほうがすっきりする。
配列一つ一つに対する処理と、色々な配列を同行する処理を分離することができるというわけだ。
この処理を分離ということ自体は、クロージャの利点というよりかは
クロージャが実現したyieldの利点とも言える。
クロージャ自体は状態をもてる関数というものなのだが、工夫次第で色々なことができそうだ。