はじめに
TECHDRIVEの小笠原です。
JavaScriptを書いていると様々な場面で見聞きする「クロージャ」というワードですが、こいつが中々のくせ者です。
調べて概要を読んでみてもイマイチ理解できないという方は、多いのではないでしょうか。
私も初めてクロージャに触れた際に、理解に苦戦したのを覚えています。
本記事は、上で述べたような経験も踏まえ、なるべくクロージャをわかりやすくお伝えすることを目的に書かせていただきました。
また、以下の理解があることを前提に本記事を書きましたので、あらかじめご了承くださいませ。
- オブジェクトを理解している(プロパティ/メソッドの用途やオブジェクト内でのthisの性質は理解している)
- 関数単位で変数のスコープが作成されることを知っている
- スコープチェーンを理解している
1. 変数の寿命を知ろう
突然ですが、ある関数に属する変数の寿命(いつまで参照可能か)を問われた時、みなさんはどのように答えますか?
正解は「変数が属する関数(本記事では親関数とする)の実行が終了するまで」ですね。
なぜこのような話を挟んだかというと、JavaScriptでは、親関数の実行後も変数を参照する手段が存在するためです。
以下のコードを見てください。
function person () { var name = 'Ken'; return { greet: function() { 'My name is ' + name; } } }; var ken = person(); ken.greet(); // 結果: My name is Ken
greetメソッドの結果は皆さんの期待通りでしょうか?
「当たり前だ」という方は、先ほど上で述べた変数の寿命は「変数が属する関数の実行が終了するまで」を思い出してください。
上記のコードでは親関数の終了後も、greetの内部から変数nameの値を参照できていることになります。
これは、関数内の変数の寿命が「変数が属する関数の実行が終了するまで」という事実と矛盾します。
詳細は後述に譲りますが、この理由がクロージャです。
2. クロージャを知ろう
さて、いよいよ本題のクロージャの話になります。
まずはじめにクロージャとは「関数がもつ性質である」ということをお伝えしておきます。
関数には「自身が定義された時のコンテキスト(スコープとほぼ同義)に存在する変数を参照し続けられる」という性質があります。 多くの場合、この性質を指してクロージャと言います。
ここで、先ほどのコードを思い出してください。
greetは親関数personの実行が終了した後も、変数nameを参照することができていました。
これは、personの返り値に含まれるgreetが関数であるため、上で述べた性質を持っていることを意味します。
greetは、自身が定義された時のスコープ内の情報を持っているのです。
故に、person実行後も、親関数(person)のスコープに存在する変数nameを参照し続けることが出来る訳ですね。
3. クロージャのメリットを知ろう
クロージャが「定義時のスコープ内の変数を参照し続ける」という関数の性質であることは、お分かりいただけたかと思います。
とはいえ、それの何が嬉しいのでしょうか?
実用的な例の一つとして、モジュールパターンが挙げられます。
例えば、プログラミング時にある用途ごとに処理を切り出したい場合、Class等を使用することが多くあります。
しかし、JavaScriptに置いてClassはES6から実装された機能であり、現状ES6が実装されていないブラウザも存在します。そのため対応ブラウザによっては、Classの使用が難しいシーンがあるかと思います。※ 但し、Babel等のトランスパイラの使用することで、この問題は解決できます。
このような背景があり、JavaScriptでは、関数を使用し「Classのようなもの(※)」を実現することがあります。
※ 本記事で述べる「Classのようなもの」とは、ある用途ごとに処理をまとめる手段を指します(継承やコンストラクタ相当の機能は持たないものとします)
その手段の1つとして、クロージャを使用したモジュールパターンが挙げられます。
例えば「URLからクエリストリングを取得して、オブジェクトに変換して返す」という処理を実装したいとします。
これを実現する方法は複数あるのですが、まずはクロージャの使用例と比較するため、オブジェクトを用いてこの処理を書いてみたいと思います。
var SplitQueryString = { init: function(){ this.queryString = location.search; }, toObj: function() { var queries = this.queryString.slice(1).split('&'); var results = {}; for(var i = 0; i < queries.length; i++) { var query = queries[i].split('='); var key = query[0]; var val = query[1]; results[key] = val; } return results; } } // ex: URLがhttp://example.com?hoge=hogehogeの場合 SplitQueryString.init(); SplitQueryString.toObj(); // 結果: {hoge: "hogehoge"}
上記のコードは一見問題なさそうに見えます。
しかし、オブジェクトはプライベートなプロパティを持つことができません。
例えば、初期化処理(initメソッド)でオブジェクトに追加しているqueryStringというプロパティがあります。
このプロパティはオブジェクト内でクエリストリング(location.searchの値)を持ち回るためのプロパティであり、初期化時(init実行時)に一度だけ値の代入を行えば良いので、以降の処理で値が変更されることを望みません。
しかし、初期化処理の実行後も、以下のようにすることでこのプロパティへの変更は可能となります。
SplitQueryString.queryString = "huge"
※ 今回のコードにおいて、queryStringにはlocation.searchの結果が代入されることが前提となっているため、上記の変更を行なった上でtoObjメソッドを実行すれば、当然エラーとなります。
上で述べた例は少々極端ではありますが、このようにJavaScriptで「Classのようなもの」を実現する際に、プロパティやメソッドをプライベートにしておきたい(外部から変更されたくない)シーンがあります。
この問題を解決する手段の一つとして、モジュールパターンがあります。
モジュールパターンではクロージャの性質を利用し、プライベートなメソッドやプロパティを実現します。
先ほどのコードをモジュールパターン使用した実装に変更してみます。
var SplitQueryString = (function() { var queryString = location.search return { toObj: function() { var queries = queryString.slice(1).split('&') var results = {} for(var i = 0; i < queries.length; i++) { var query = queries[i].split('='); var key = query[0] var val = query[1] results[key] = val } return results } } })() // ex: URLがhttp://example.com?hoge=hogehogeの場合 SplitQueryString.toObj() // {hoge: "hogehoge"}
上記のコードでは、変数SplitQueryStringに即時関数を代入しています。
※ 即時関数がオブジェクトを返しているため変数SplitQueryStringの値は、オブジェクトということになります。
1つ目のコードとの大きな違いとしては、以下の2点が挙げられます。
- オブジェクトを即時関数でラップして返している
- 即時関数の中でinit相当の処理を行なっているため、返り値となるオブジェクトからinitメソッドが消えている
ここで注目すべきは、1つ目の差分です。
なぜ、オブジェクトを即時関数でラップしているかというと、返り値であるオブジェクト内のメソッド(関数)のクロージャの性質を利用するためです。
toObj(クロージャ)は、親関数(即時関数)の実行終了後も、定義時のスコープ内に存在する変数queryStringを参照し続けることができます。
※ ややこしいのですが、クロージャの性質を利用することを目的とした関数自体を指してクロージャと呼ぶこともあるため、toObjをクロージャと表記しています。
queryStringはクロージャが内側で保持している変数であるため、クロージャ以外からは参照/変更することができません。
このクロージャの性質により、関数でプライベートなプロパティやメソッドを実現することができます。
4. まとめ
いかがでしょうか。
クロージャは、その性質を見聞きしただけでは理解が難しく、JavaScript中級者へのステップアップにあたり、鬼門となりがちです。
本記事が少してでもクロージャへの理解にお役立ていただけたのなら幸いです。
また、今回はクロージャのメリットに焦点を当ててきましたが、クロージャがもたらすものは、メリットだけではありません。
本記事でもお伝えした通り、クロージャの「スコープ内の変数を参照し続ける」という性質は、見方を変えると本来関数の実行終了と共にメモリ上から消えるはずのデータが、残り続けることを意味します。
この点に関して、本記事で詳しく触れることはしませんが、この性質はメモリリーク等を引き起こす要因にもなり得ます。
そのため、JavaScriptの担う領域が大きければ大きいほど、クロージャへの正しい理解が求められます。
PR
TECH DRIVE協賛企業のサークルアラウンド株式会社では、プログラマーの成長を加速させるためのトレーニングを行なっています。フロントエンド/バックエンド問わず各種バリエーションがございますので、ご興味がある方は是非以下のリンクより詳細をご覧ください。
JavaScript Climbing
JavaScriptに特化したトレーニング「JavaScript Climbing」を行なっています。
this、クロージャ、Class等、本格的なフロントエンドの開発に臨むにあたり、必須となるスキル習得を現役のWEBエンジニアが徹底してサポートします。
無料体験期間もございますので、ご興味がある方は是非以下のリンクより詳細をご覧ください。
個別トレーニング
短期間でぐっと成長したい方は弊社主催の個別トレーニングがおすすめです。 トレーニング内容は、受講者の方の課題/要望をお伺いした上で、フルオーダメイドで作成させていただきます。 詳細は以下のリンクよりご確認ください。(応募者多数の場合には時間を別途ご用意する予定です)。