クロージャが理解できない人のための超クロージャ入門。
なんだか全然違う事を考えていて、おもむろにクロージャまで考えが到達してハッと喉のつかえが取れたのでメモ。
タイトルは釣りです。嘘ですちゃんと説明してます。自分なりに。
あとからタイトルが×「超クロージャ入門」→○「クロージャ超入門」にしなきゃいけなかったと気付いたのは秘密。
前座
理解したつもりでいながらも、自分の中で整理しきれなかった項目がクロージャでした。
で、ついさっきやっと、難しく考えすぎていたなぁと気づいたので、自分として理解した事をメモがてら説明しておこうと考えました*1。
といっても、目新しい説明は何も書いてないです。小難しい理屈とか、実装を先に見てしまうからわけわからなくなるんだと思ったので、間違っているかも知れないぐらい大雑把な説明*2と、得られる結果だけを先に書くという説明手法で書いたというだけです。
こういうエントリって、もっといい説明の劣化物を生産することになるのは承知してるけど、人に説明することは自分の理解を深めると思うので、あえて書きました。
以下本題。
クロージャって、どういうものなの?
えっと、オブジェクトとどう違うの?とかは今は頭から捨ててください。
一口で言うと、「自分というものを認識してる関数」です。
では、実装は放置で、先に実行内容と結果だけ見てみましょう。
結果がalertなのかdocument.writeなのかとかそういう細かいことは全部放置。わかれw
とにかく、以下のようなカウンターが実装できてるとします。
c1 = counter(); c2 = counter(); c1(); c1(); c1(); document.write("<br />"); c2(); c2(); c2();
で、以下結果
// 結果: 1 2 3 1 2 3
でっていう。
いや、そんだけです(笑)
ごめん笑うとこじゃなくて、本当にそれだけです。
じゃ、簡単に上のコードをどう受け止めればいいのかお話をしていきましょう。
まずはcounter()って何してるかの挙動を理解しる。
まず、そもそもc1とc2両方考えるんじゃなくて、c1だけ見ましょう。
ここではなにが起きてるかというと。。。counterという関数から「何か」が返り値として返されていて、その「何か」がどうも実行可能な関数なのです。
慣れてない人には「関数が返ってくる」という感覚自体が難しいと思いますが、とにかくそうなってます。
だから
c1 = counter();
c1();
// 結果:1
ってことができてる。1行目で返ってきた「何か」が関数だから、2行目で「関数の実行」ができる、という仕組み。
この点をまず理解してください。
この、「何か」関数に名前はありません。とにかく「何か」関数が変数に格納されてる、と。
変数に()付けて名もない関数を実行だなんて、なんときもいんでしょう><とかは我慢。
キモイ気持ちはとにかくそのままとっといて読み進めるべし。
でまぁ、この「何か」関数は実行すると数値を表示する、と、そういう関数っぽいですね。
知ってる人には冗長かもしれませんが、そういうのは我慢しる。超入門つったべ。
なんか同じ関数の結果が毎回違くなってることを理解しる。
さて、ではつづけて
c1 = counter(); c1(); c1(); c1(); // 結果: 1 2 3
って結果が増えていく点に関して。
・・・わかるよね?結果が増えていくんですよ。
どういう仕組みなんだかねw謎関数w
とにかく、この「counterという関数から返り値として渡された謎の関数」は、1回実行する毎に実行結果として表示される数値が1ずつ増えていく。そういう関数になっている。
それって関数なの?とか思うよねwなんときもいんでしょう><とかいう思いは(ry
またキモイ気持ちはぐっとそのまま押しこめて(ry
とにかく、1回実行する毎に結果が1ずつ増えますよ、と。
じゃあc2も混ぜて考える。
さてさて。
やっと併せて理解をするところに来ましたよ、と。
話が長い?だから超入門だと何度言えばほげほげ。
c1とc2では、実行結果が別々にカウントされてましたね。
// 結果: 1 2 3 1 2 3
てなってた部分の話。
でもでもよく考えると、これって当たり前のような当たり前でないような。
両方とも、counter関数から返り値として渡された謎関数なわけです。
で、そいつらが二つとも別々に、自分勝手に数を数えてるわけですね。
・・・ふつうじゃね?
だってさ、counter関数の「返り値」なわけじゃんかw
そりゃー別々のものでしょwww
とはいえ、これが普通だと思えない人もいたらアレなので、以下に簡単に。
funciton ret10() { return 10; } a = ret10(); b = ret10(); a = a + 1; alert(a); alert(b);
で。
結果: 11 10
。。。。わかるよね?
aもbも最初の値の起源はret10という関数だけれども、返ってきた結果の値はもう、その関数とは独立しているわけです*3。
これは直感的ですよね。
c1とc2の数のカウントが独立なのも、それと同じことです。
で、つまるところクロージャって何なんだと。
最初に言った事を思い出してください。
そして、ここまでに抱えたもやもやも思い出してください。
今までにわかったことをまとめると
- なんか、関数の返り値として変数に格納されたのが、名もない謎関数だった。
- その謎関数は実行する毎に返り値が規則的に変わってる。
- 元関数と、返り値の謎関数は独立っていうかほぼ無関係。他の返り値とも無関係。
それで、今回やった中で「クロージャ」と呼ばれるものは、この謎関数のことなわけです。
こういう挙動をするものが、クロージャというものなわけです。
各項目を、この理解をベースに掘り下げていけば
- 関数の返り値として、クロージャが返ってきて、変数に格納された。
- 返り値であるクロージャ本体に名前は無い。でも、実行はできる。
- クロージャは実行する毎に返り値が変わっている。
- 返り値が変わっているということは、自分の中に何か「内部状態」を持っている。
- そして、クロージャが実行されるたびに、その内部状態が変わっている。
- クロージャは元の関数とも、他のクロージャとも無関係。
という感じに、掘り下げて理解することができる。
これを一口で言おうとしたときに
- クロージャとは、「自分というものを認識してる関数」
という事になるわけです。なんか今振り返ると、この表現もちょっと微妙ですねw
じゃあ、そろそろcounter関数の実装がどうなっていたかの話でも
やらないか*4。
ということで、今までcounter関数の内部実装がどうなっているのかを完全に無視して話を進めてきましたが、ここでやっと、長々引っ張ったクロージャの実装と合わせて、最初のスクリプトを再掲。
function counter() { var count = 0; return function () { document.writeln(++count); }; } c1 = counter(); c2 = counter(); c1(); c1(); c1(); document.write("<br />"); c2(); c2(); c2();
上部のcounter関数の部分が、クロージャの実装部分です。
これと、さっきの理解を組み合わせてみましょう。
返り値であるクロージャ本体に名前は無い。でも、実行はできる。
なんかreturn function() とかいう記述がありますね。
これは正しくは無名関数とかlambda式とか呼ばれる物ですが、こうやって関数を丸ごとreturnできる仕様になっているのです。
名前はありません。無名関数ですから。
counter関数は、この無名関数を返り値として返す関数なわけです。
返り値が変わっているということは、自分の中に何か「内部状態」を持っている。
なんかvar count = 0;とかなってますね。counter関数の側で。
そりゃそうです。無名関数の中でvar count = 0;ってやったら、実行する毎にリセットされちゃいます。
いや、それはそれでありだけどさ、それって普通の関数ですよね。
無名関数を返すcounter関数の側で変数を定義してるのがミソです。
そして、クロージャが実行されるたびに、その内部状態が変わっている。
と、そのミソをふまえて考えると、わかりますよね。
無名関数自体は、count変数を増加させることしかしてません。
それぞれのクロージャは、独立。
各クロージャが「独自に」自分専用の内部状態を持っている。
これって、実装を見るより最初の理解に立ち返ってくれた方がずっといいですね。
(別に参照とかポインタとかのつながりも何もない)関数の返り値なんだから、普通に考えてそれぞれが独立なんです。
なので、どうして
c1(); c1(); c1(); document.write("<br />"); c2(); c2(); c2();
が
1 2 3 4 5 6
じゃないの?なんていうのは、おかしな疑問ですよね。
むしろ上記のように続いたら、返り値同士が独立じゃないわけで。
それは何か、中で参照渡しのような形で使いまわされてるわけですよね。
その辺のことを合わせたクロージャのイメージ
上の理解を軽くまとめると、クロージャはどうも「無名関数+α」って感じなのがわかりますね。
で、そのαってのが、クロージャのちょっと外にある、counter関数で定義されてるローカル変数なわけです。
これって、すごく近い概念があることに気付きませんか?
そう(`・ω・´)シャキーン
オブジェクトです。
オブジェクトは「プロパティ+メソッド」という、ほっとんど同じような構成をしています。
しかも、クラスに対してnewするとインスタンスが返るのと、counter関数を実行すると無名関数が返るのって、イメージが極めて近いですよね。
わずかに違いがあるとすれば・・・そのつくりの荒っぽさが違う感じがしますね。
オブジェクトの方は、すごく整ったイメージがしませんか?
プロパティもメソッドも、きちんと整った枠のかに両方収めて、塊として扱います!って作ったイメージ。
プロパティとメソッドが並列な立場にあって、両方を好きなだけ用意できます。
かたや。
クロージャの方は、まず前提として無名関数ありきですよね。
なにしろ無名関数をreturnするので、メソッドに当たるものは1つしかありません。
一応、内部で使う変数は、いくつも用意できそうですが。
無名関数があって、そこにあとからcount変数をはんだ付けしたような、そんなイメージがしませんか?
その感覚が、クロージャです。
そろそろ、クロージャのイメージが頭の中で出来上がりつつあるのではないでしょーか。
そうじゃないんだと、もう僕の説明がへたくそ過ぎたってことですね(´;ω;`)ウッ…
で結局、クロージャってなにがいいの?
オブジェクトっぽいものをオブジェクトより簡単に書けます。
以上。
でっていう。
いやマジですからw本当にそういうものです。
だから、クロージャはオブジェクトで代替が利きます。
そういうものです。
オブジェクトが存在する言語でクロージャも存在することの利点は、クロージャにしかできないことがあるからじゃないんです。
クラス書くのがマンドクサイけど関数じゃ微妙にできない、ってのを簡単に書けることなんです。
手続き型にある程度慣れた人ならわかるでしょうが、正直クラス書くのって大変ですよね?
関数に切り出すのはすごく簡単なのに、オブジェクトとして切り出すとなると、やけにコスト高まりませんか?
このコストの差。この間を埋めてくれるのが、クロージャの役割なんです。
リッチさの比較でいえば
関数 < クロージャ < オブジェクト
という順番が成り立つような存在だったというわけです。
つまるところ、クロージャとは、
関数とオブジェクトの中間ぐらいに位置する、「自分のことは認識できる関数みたいなもの」
だったということです。