こんにちは!Webコーダー・プログラマーの貝原(@touhicomu)です。
みなさん、クロージャって聞いたことありますか?
JavaScriptのコードを書く上で必要不可欠な知識です。
そんなの聞いたことない…
聞いたことあるけど、よくわからない…
今回は、そんな悩みを解決していきましょう。
JavaScriptのクロージャについて解説します。
この記事では下記の流れで、応用的かつ実践的な内容まで解説していきます。
【基礎】クロージャのメリットとは?
【基礎】クロージャの使い方
【発展】クロージャの活用例2つ
【発展】メモリリークには気をつけよう
クロージャは少し特殊なオブジェクトで、解説だけではイメージがしづらいかもしれません。
本記事では具体的なコード例をふんだんに使い、順を追ってしっかり解説します。
安心して、しっかり学習していってください!
クロージャの基本
クロージャとは関数とその関数が定義された状態をセットにした特殊なオブジェクトのことです。
ものすごくざっくり言うと、下記のように、変数の中に変数が入っている状態を指します。
var createTimer = function () { var time = 10; return function timeDown() { time -= 1; console.log(time); }; };
これだけではちょっとわかりづらいですね。
クロージャと通常の関数のサンプルコードを比較しながら、違いを理解していきましょう。
クロージャとは?
通常、関数を書く時には下記のように記述しますね。
[通常の関数]
function timeDown() { var time = 10; time -= 1; console.log(time); } timeDown(); // 「9」 timeDown(); // 「9」のまま timeDown(); // 「9」のまま
[実行結果]
9 9 9
この関数では、timeDown()がなんど呼ばれても、タイマーの値が入った変数(time)は9のままです。
呼び出される都度、変数timeは10で初期化されたあとに、-1されるため、得られる結果は変わらず9のままになります。
では、クロージャを使った例を見てみましょう。
[クロージャ]
var createTimer = function () { var time = 10; return function timeDown() { time -= 1; console.log(time); }; }; //グローバル変数にセットされたことで、クロージャになる var timer = createTimer(); timer(); // 「9」 timer(); // 「8」 timer(); // 「7」
[実行結果]
9 8 7
クロージャの例の場合、実行結果として9,8,7とtimer()が呼ばれるたびに結果が変わっているのがわかりますね。
これは、関数(createTimer())の中で定義された変数(time)と関数(timeDown())の結果が、セットで保存されているためです。
この、セットで保存されるという現象そのものがクロージャです。
クロージャのメリットとは?
クロージャのメリットは、オブジェクトの変数やメソッドを他のプログラムから簡単に変更されないように制御できることです。
ソース量が多かったりチームで開発をする場合は、自分のプログラムで使用しているオブジェクトの変数の値などが他のプログラムで容易に書き換えできるようになっていると、予期せぬエラーやバグが発生する可能性があります。
そのため、カプセル化と呼ばれる、オブジェクト内部で使用している変数やメソッドを容易に変更できない仕組みが存在しています。
クロージャはその仕組みの一役を担っているのです。
クロージャの使い方
では、クロージャはどのように記述するのかを確認していきましょう。
基本形は下記の形です。
var メソッド名 = function(引数) { // 処理 }
varを使ってメソッドを変数として代入しているところがポイントです。
こうすることにより、クロージャとして、カプセル化して外部に公開しないメソッドにすることができます。
ちなみに、プロパティの場合は下記のように書くことでカプセル化することができます。
var プロパティ名 = 値
以上を踏まえて、サンプルコードを見ていきましょう。
window.onload = function () { // クロージャによるカプセル化の書き方 // オブジェクトの定義 var Hello = function (_name, _major) { // カプセル化されたプロパティ(変数) var name = _name; // 公開されているプロパティ(変数) this.major = _major; // カプセル化されたメソッド var getName = function () { return name; } // 公開されているメソッド this.setName = function (_name) { // 代入前に加工ができる name = 'Mr.' + _name; } // 公開されているメソッド this.getMajor = function () { return this.major; } // 公開されているメソッド this.say = function () { console.log('Hello! ' + getName() + '. I know you are great about ' + this.major + '!'); } } }
上記の例では、各メソッドは全てクロージャになっているので、Helloオブジェクトの変数に自由にアクセスできます。
変数nameを決める関数は、getNameと、setNameの2つがあります。
このうち、getNameはカプセル化されていて外部からはアクセスできません。
一方変数majorは、getMajorと、getMajor2つの関数のどちらも外部に公開されているので、外部からのアクセスが可能です。
最後にsayメソッドが、メッセージをコンソールに出力します。
では、このコードを実際に動かしてみましょう。
var ins = new Hello('Jobs', 'Design'); ins.say(); ins.name = 'Tim'; // 反映されない(カプセル化されている。アクセスできない。) ins.major = 'Computer Science'; ins.say(); ins.setName('Tim'); // 反映される。(セッターメソッドを使用) ins.major = 'Marketing' ins.say();
実行結果
Hello! Jobs. I know you are great about Design! Hello! Jobs. I know you are great about Computer Science! Hello! Mr.Tim. I know you are great about Marketing!
「ins.name = ‘Tim’」というコードは反映されていませんね。
これは、変数nameがカプセル化されていて、変更することができないからです。
しかし、setNameはカプセル化されていないので、変数nameの値を変えることができます。(実行結果の3行目の出力です)
ここまでで、クロージャがどのように動作するのかを確認できましたね。
次の項目では、クロージャのスコープについて解説していきます。
スコープってなんだ?という人は、まずは下記の記事をチェックしてみてください。
>>初心者必見!スコープ徹底解説
クロージャのスコープ
関数のスコープは、関数の定義された内側のスコープを関数のスコープとしますが、クロージャは少し違います。
クロージャでは、クロージャが定義された外側のスコープをクロージャのスコープとして使用することができるのです。
下記の例で、クロージャが定義された外側の変数にアクセスできることを確認してみましょう。
window.onload = function () { // クロージャの基本 // クロージャの書き方とスコープ function outerFunc(param) { var str = param.toString(); // クロージャを関数オブジェクトとして変数に代入 var closure = function () { // closureと同じスコープの変数strにアクセス可能 var ret = "---" + str + "---"; return ret; } // クロージャの戻り値をouterFuncの戻り値にしている。 return closure(); } console.log('outerFunc("abc") = ' + outerFunc("abc")); }
実行結果
outerFunc("abc") = ---abc---
outFuncに渡された”abc”がoutserFunc内で変数strに代入されていますね。
そして、クロージャclousureは、自身の外側にある変数strにアクセスし、出力を行なっています。
これはクロージャの特徴の1つで、普通の関数の場合は変数strにはアクセスできません。
即時関数のスコープ
即時関数とは、名前の通り即時に実行される関数のことです。
下記のように関数を書くと、即時関数としてすぐに実行されます。
(function() { // 実行内容 }());
詳しいことは下記の記事で解説していますので、併せて読んでみてください。
では、この即時関数でのクロージャのスコープについて解説していきましょう。
クロージャを即時関数内のスコープで定義すると、クロージャは即時関数内のスコープの変数にアクセスできます。
しかし、即時関数外のスコープからは、即時関数内のスコープの変数にはアクセスできません。
例を見てみましょう。
window.onload = function () { // クロージャと即時関数のスコープ // 即時関数スコープ (function () { // 即時関数内変数 var immidateValue1 = "immidiate"; // クロージャ function immidiateScope() { // 即時関数内スコープの変数にアクセス可能 return "***" + immidateValue1 + "***"; } // クロージャの結果 console.log("immidate return = " + immidiateScope()); })(); // 即時関数外スコープからは、即時関数内スコープの変数にはアクセスできない console.log("immidateValue1 = " + immidateValue1); }
実行結果
immidate return = ***immidiate*** Uncaught ReferenceError: immidateValue1 is not defined
クロージャは即時関数内の変数にアクセスできていますね。(実行結果1行目)
一方、即時関数外のスコープから即時関数内のスコープの変数にアクセスしようとするとエラーが出てしまうのです。(実行結果2行目)
クロージャを使う際には、どこのスコープが使えるのかを注意するようにしましょう。
クロージャの実践
では、クロージャを使った実用的な例を見ていきましょう。
もちろん、クロージャの活躍箇所はここで紹介した例だけではありません。
例を参考に、ぜひ色々な組み合わせで試してみてください。
onloadと組み合わせてみよう
データが読み込まれたタイミングで実行されるonloadイベントにクロージャを設定する方法は色々あります。
そのうちの一つとして、関数の戻り値にクロージャを渡すという方法があります。
クロージャ内では、渡す側の関数の変数にアクセスできるからです。
以下、例を見ながら確認していきましょう。
// クロージャによるイベントハンドラの書き方 // onloadにクロージャを設定し使う方法 function makeOnLoadCallback(name) { var cnt = 0; // クロージャを戻り値にする return function () { cnt++; console.log('onload event named as "' + name + '" fired! at ' + cnt + ' times.'); }; } window.onload = makeOnLoadCallback('myOnLoad');
実行結果
onload event named as "myOnLoad" fired! at 1 times.
上の例では、onloadにmakeOnLoadCallback関数を定義しています。
makeOnLoadCallback関数の戻り値はクロージャになっていますので、onloadが実行される際は実質上クロージャがコールバックされます。
さらに、makeOnLoadCallbackの引数nameに値(myOnLoad)を渡していて、クロージャはそれを参照してコンソールへ出力するという仕組みになっています。
onclickと組み合わせてみよう
クリックした時に発火するonclickイベントにクロージャを設定する例として、まず、HTMLの方を見てみましょう。
以下のHTMLのdivタグをクリックの対象とします。
<div id="log"> ログエリア </div>
次に、前項と同様に関数の戻り値としてクロージャを渡していきます。
window.onload = function () { // クロージャによるイベントハンドラの書き方 // onclickにクロージャを設定し使う方法 function makeOnClickCallback(msg) { // メッセージ var msgStr = msg.toString(); // クロージャを関数オブジェクトとして変数に代入 var closure = function () { // クロージャの処理 console.log('Alert! You are warned as "' + msgStr + '". '); } // クロージャを戻り値にする return closure; } // クリックするエレメント var el = document.getElementById('log'); // onlickイベントハンドラにクロージャを登録する el.onclick = makeOnClickCallback("Do not bother me!"); }
実行結果(divを3回クリックした場合)
Alert! You are warned as "Do not bother me!". Alert! You are warned as "Do not bother me!". Alert! You are warned as "Do not bother me!".
こうすることで、HTMLで設定したdivタグのエリアをクリックする度に、コンソールに引数msgの値とともにメッセージを表示することができるのです。
メモリリークするクロージャの書き方
プログラムコードを組んでいると、メモリの解放し忘れであるメモリリーク(メモリ漏れ)が起きるコードを書いてしまうことがあります。
メモリがリークすると、リークすればリークした分だけサーバーやパソコンが搭載しているメモリの使用量がどんどん大きくなってしまいます。
そして最後にはメモリの空き容量がなくなってサーバーやパソコンがダウンして止まってしまうのです。
クロージャにより外側のスコープの変数をうっかり保持したままでいると、クロージャが存在している間中ずっとその変数がメモリ内に残り続けてしましまいます。
これがメモリリークです。
特に、スコープの関係上、どの変数をうっかり保持したままなのかわかりづらい場合もありますのでやっかいです。
以下の例では、bigDataという大きなメモリ容量を占めるデータを、オブジェクトmakeRefHavingClosure経由でクロージャがずっと保持し続けています。
window.onload = function () { // メモリリークする場合のクロージャの書き方 // メモリリークする変数を参照するオブジェクト function makeRefHavingClosure(reference) { // 変数の参照を持ち、メモリリークする原因となるクロージャ this.log = function () { console.log('leak val is ' + reference.toString()); } } // とても大きなリークするデータ var bigData = "Too Most Ammount Big Data ..................."; // インスタンス化 var ins = new makeRefHavingClosure(bigData); // リークするデータを消去したハズ delete bigData; // 消去したハズのデータを操作できる(メモリリークしている) ins.log(); }
実行結果
leak val is Too Most Ammount Big Data ...................
上の例では、bigDataを途中でdeleteしてメモリ上から削除していると見せかけて、クロージャがまだ参照しているため、メモリからは削除されていません。
結果、エラーが出てしまっていますね。
この現象に陥らないよう、クロージャを使う際には十分注意しましょう。
まとめ
いかがでしたか?
今回の記事のポイントは
・クロージャは、オブジェクトのカプセル化に非常に役立ちます。
・クロージャをイベントハンドラに登録し、コールバックさせることもできます。
・クロージャは扱い上、メモリがリークしてしまう危険性があります。
という点です。
クロージャは、JavaScriptのコードを組む上で必須の知識です。
ポイントを抑えて、しっかりと覚えましょう。
うっかり忘れてしまったら、またこの記事を読み返してください!