「JavaScriptを理解する」第3回です。今回は「クロージャ」というものをメインに勉強してきました。
また、クロージャを理解するためには、JavaScriptで採用されているスコープについても詳しく知っておく必要がありそうだったので、そちらも併せて勉強しました。
まずはスコープについて触れてから、クロージャについて学んだことをまとめていこうと思います。
JavaScriptのスコープ
まずはJavaScriptで採用されているスコープ方式についてまとめていこうと思います。
スコープとは
そもそも、スコープとはなんだ?と調べてみると、 IT用語辞典 さんでは以下のように定義されていました。
スコープとは、プログラム中で変数名などのシンボルが参照可能な有効範囲のこと。
宣言した変数やオブジェクト・関数などをどこまでの範囲で呼び出したりすることができるのか?という領域がプログラミング言語によって決まっていて、その範囲のことをスコープと呼ぶそうです。
JavaScriptのスコープ
JavaScriptには、「 グローバル」と「 ローカル」の 2 つのスコープがあります。
どの関数定義の内側にも属さない場所で宣言された変数は「 グローバル変数」と呼ばれ、 プログラム内のどこからでも値の参照や呼び出し・変更を行うことができます。
逆に、 関数定義内で宣言された変数は「 ローカル変数」と呼ばれ、 関数の外部からローカル変数にアクセスすることはできません。また、 関数が実行されるたびに作成され破棄されます 。
この他、ブロックスコープというのがES6からサポートされています。変数宣言時、letやconstを使うとブロックスコープが有効になる、という話を 第一回目で触れました。
例:グローバル変数とローカル変数
var hoge = "ほげ"; //グローバルスコープ
function a(){
var x = 10; //ローカル変数
console.log(x); //-> 10
console.log(hoge); //->ほげ(グローバル変数が呼び出される)
}
function b(){
var hoge = "ホゲッ"; //ローカル変数(グローバル変数のhogeとは別物)
console.log(hoge); //-> ホゲッ(ローカル変数のhogeが呼び出される)
}
console.log(hoge); //-> ほげ
console.log(x); //->ReferenceErrorエラー
グローバル変数となったhogeは、どの関数の内側でも外側でも参照可能です。関数内で同名の変数が宣言されると、グローバル変数とは全く別物のローカル変数が作成されます。参照には優先度があり、一番近いスコープの中で変数を参照します。
また、ローカル変数をグローバルレベルで呼び出すなどの、有効スコープ外での参照は ReferenceErrorエラーとなり、not defined(こんな変数定義されてないよ!)と注意されます。
レキシカルスコープとダイナミックスコープ
さて、JavaScriptのスコープにはグローバルなものとローカルなものがあるのだと学びました。
しかし、細かいことを気にして行くと、ローカル変数の有効範囲は関数の定義時に決まるものなのか、他の場所で呼び出した時にも更新されるものなのか、という疑問が発生しませんか?(私はしませんでした。笑)
これについては、実は各プログラミングごとに決まっていて、それぞれ「 レキシカルスコープ(静的スコープ)」と「 ダイナミックスコープ(動的スコープ)」と呼ばれています。
JavaScriptやその他多くのメジャーな言語(Ruby、Java、python等)では前者の レキシカルスコープが採用されています。
ダイナミックスコープが採用されている言語には、Perlなどがあるようです。
レキシカルスコープとダイナミックスコープについて、少しまとめていきます。
レキシカルスコープ
レキシカルスコープは 関数を定義した時点でスコープが決まります。
コードを見ながら理解するのが早いと思うので、いきなりですが以下のコードをご覧ください。
var x = 10;
function A(){
console.log(x); //この時の静的なスコープはx=10
}
function B(){
var x = 1000; //ここでもxが定義されている
A(); //この時のxは10?1000?
}
A(); //10
B(); //-> 10 (1000ではない!)
関数Aは変数xを出力する関数です。関数Bではxを再定義しており、関数Aを呼び出しています。
「A();」をそのままグローバルレベルで呼び出すと、もちろんグローバル変数のx=10が返されます。
では、関数Bの中で呼び出されるとどうなるのでしょう?
関数Bの中で呼び出される関数Aによって出力されるxはどのxを参照するのか?というのがスコープによる違いで変わってきます。
レキシカルスコープでは関数Aで出力するxのスコープは、関数Aが定義されいた時点で決定し、そのまま保持されます。
つまり、呼び出し元の関数Bで新たにxが定義されていようとも、関数Aが定義された時点で参照していたxが出力されます。
上記の例では関数Aの定義時に変数xが参照するのはグローバル変数のx( = 10 )であるので、関数Bの中で関数Aを呼び出したとしても、返ってくる値は10のままとなります。
ダイナミックスコープ
ダイナミックスコープは、 関数を実行した時点でスコープが決まります。
という風によく表現されています。
Wikiを見てみると、
動的スコープは、静的スコープ(構文構造のみから決定できるスコープ)に加え、実行時の親子関係の子側(呼び出された側)から親側(呼び出し側)のスコープを参照できるスコープである。
と説明されています。つまり、関数を実行した時点でスコープが決まるというより、定義時にもスコープが形成されているが、 関数の呼び出しによっても新たに再形成される、という表現の方が近いのかもしれないです。
さて、先ほどJavaScriptのコードを例にレキシカルスコープの挙動を見てみましたが、 もし仮にJavaScriptがダイナミックスコープであれば結果がどう変わるか、同じコードを例に見てみましょう。
例:もしJavaScriptがダイナミックスコープである場合
var x = 10;
function A(){
console.log(x); //この時の静的なスコープはx=10
}
function B(){
var x = 1000; //ここでもxが定義されている
A(); //この時にスコープが再形成される
}
A(); //->10
B(); //-> 1000 (10じゃなくなった!)
グローバルレベルで呼び出している「A();」による出力結果は変わらず 10 ですね。この時にもスコープは再形成されていますが、参照できるのはグローバル変数だけなので結果には違いが見られません。
関数Bの実行時はどうでしょうか。この時、関数Aは関数Bの内部で呼び出され、スコープも再形成されます。
これにより関数Aで出力される変数xは、関数Bで定義されているローカル変数x(=1000)を参照するようになります。
JavaScriptのクロージャ
いよいよ本題。クロージャについてまとめていきます。
いきなりクロージャのことを理解しようとすると複雑に思えますが、先ほどのレキシカルスコープの性質を理解していればそんなに難しくはないです。たぶん。
クロージャとは?
クロージャとはなんだ?と調べてみると、
「JavaScriptの関数は全てクロージャ」と表現されているのをよく見かけます。
ですが、実際には少し違うようです。「 JavaScriptの 関数は全てクロージャになり得る 」と表現すべきでしょうか。
私が最終的にしっくりきたのは「 MDN | クロージャ」にてさらっと書かれていた以下の定義です。
クロージャは「関数」と「その関数が作られた環境」という 2 つのものが一体となった特殊なオブジェクト
この「 環境」というのは、クロージャが作られた時点でスコープ内部にあったあらゆる変数や関数・オブジェクトなどによって構成されます。
じっくりコードを見て理解を深める
定義を見るだけでは全く意味がわかりません。笑
また、各所で様々な例が挙げられていますが、 コードをさらっと読むだけでは間違った解釈で覚えてしまう危険があるので注意です。(私がそうでした)
なので、まずは一つの簡単な例をじっくりと見ることで、クロージャとは一体どういうものなのか理解を深めていきましょう。
以下のコードをご覧ください。(MDNのサイトに載っているコードにconsole.logを足したものです)
function fnA() {
var hoge = "ほげ";
console.log(hoge);
function fnB() {
alert(hoge);
}
return fnB;
}
var myFunc = fnA();
myFunc();
上のコードは、関数がクロージャとして機能している、とてもシンプルなコードです。
まず初めに、何がクロージャなのかを確認しておきますが、ここでクロージャとなるのは、「 myFunc(関数fnB)」です。別の言い方をすると、 関数fnBがクロージャとなってグローバル変数myFuncに代入されています。
それともう一つ、このコードの実行結果を先に確認しておきます。
- コンソールに"ほげ"という出力がされる
- アラートで"ほげ"とメッセージが出現する
という二点です。が、以下のことに注意しましょう。
最後の行の「 myFunc();」によって実行されているのは アラートだけあり、 コンソールへの出力は「 var myFunc = fnA();」の「fnA();」よって実行されています。
すごく普通のことですが、この辺を勘違いしてしまうだけでもクロージャはややこしくなってしまうことがあるので、ひとつひとつ丁寧に確認してみてください。
もう一度myFunc();を呼び出してあげると、コンソールの出力はなく、アラートで"ほげ"と出てくるのみです。(実際にコードを動かすとよくわかります)
さて、コードの中身を確認していきましょう。
fnAという名前の関数を定義し、その中で ローカル変数hogeが文字列 " ほげ" として定義されています。また、 fnBという名前の関数も 関数fnA内部で定義されています。
この ローカル変数hoge は 関数fnA内部のローカル変数であるので、 関数fnAの実行中以外では参照することはできないはずです。
「JavaScriptのローカル変数は、関数が実行されるたびに作成され破棄される」というのを冒頭の「JavaScriptのスコープ」の章で確認しましたね。「関数内部のローカル変数は、その関数が実行されている間だけ存在する」とも表現されたりします。
では、上記の例で 関数fnAを実行しているのはどこかというと、 変数myFuncが定義された時だけです。
つまり、 この瞬間でのみ、 ローカル変数hogeを参照することができます。( 重要ポイント)
関数fnAの中身を見ていくと「 return fnB;」 とありますので、 グローバル変数myFuncに代入されるのは、 関数fnBということになります。
「 var muFunc = fnA();」の部分を書き換えてみると、
var myFunc = function fnB() {
alert(hoge);
}
という構造ですね。
つまり、最後の行「myFunc();」を書き換えてみると、
alert(hoge);
ということになります。これがグローバルレベルで呼び出されています。
そしてそして、この実行によりアラートで " ほげ" と表示されました。
...あれ?...あれあれ?
関数fnAが実行されていないのに、関数fnA内部のローカル変数hogeを参照できている ことに気付いたでしょうか?
これが、クロージャとしての重要な性質になります。
一度整理してみる
ここまでの流れを一度整理してみましょう。
- 関数fnAの内部に ローカル変数hogeが "ほげ" として定義されている。
- この ローカル変数hogeを参照できるのは本来、 関数fnAの実行時のみ
- その 関数fnAの実行は グローバル変数myFuncが定義された時のみ。
- 関数fnAの実行により、 ローカル関数fnBが myFuncに代入されている。
- myFunc( 関数fnB)を実行すると、"ほげ" とアラートされた。
つまり、 myFuncが定義された時、「変数hogeの中身は "ほげ" だ」という 情報を記憶した状態で関数fnBが代入されていたということです。(この瞬間は 関数fnAの実行中なので「ローカル変数hoge="ほげ"」への参照が可能)
ここでクロージャの定義を思い出してみましょう。
クロージャとは、「 関数」と「 その関数が作られた環境」の2つをもつ特殊なオブジェクトのことでした。
今回の例に当てはめてみると、 myFuncは「 関数fnB」と「 関数fnBが作られた時の環境(ローカル変数hoge(="ほげ")への参照)」の2つを持つオブジェクト、つまり クロージャとして定義されていたことがわかります。
別の言い方をすれば、関数fnAの実行によって 関数fnBがクロージャとなって変数myFuncに代入されたという感じでしょうか。
関数がクロージャになる条件
では、関数がクロージャへと変化するのはどのような時なのでしょうか。その条件も確認しておきましょう。
個人的に一番しっくりきたのは以下の定義でした。
関数は定義時のコンテキストとは異なるコンテキスト上に持ち出されるとクロージャになる
コンテキスト、と見慣れない言葉が出てきましたが、「関数は 自身が定義されたスコープの外部へと持ち出されるとクロージャとなる」と表現してもいいかもしれません。
こちらも今回の例に当てはめてみましょう。
関数fnA内部の ローカルスコープで定義された関数fnBは、関数fnAの実行によって、 グローバルスコープにある変数myFuncへと持ち出されたことによって、クロージャへと変化したのです。
エンクロージャとは
今回の 関数fnAのような 親関数の実行によって、ローカル関数が外部へと持ち出される時にクロージャが生成されることがここまでで分かりました。
このような、クロージャを生成できる親関数のことを「 エンクロージャ」と呼んでいるサイトがいくつかあったので、メモしておきます。
クロージャの性質まとめ
最後に、ここまで学んだ内容をもう少し一般化した表現でまとめてみます。
関数内部で定義されたローカルな関数がその親関数の実行によって外部スコープへと持ち出される時、その瞬間の環境を記憶したクロージャという特殊なオブジェクトへと変化する。
クロージャのメリットと使用例
さて、ここまでクロージャとはどういう性質を持つのかを見てきました。
次は、クロージャを使用すると何が良いのか、メリットを見ていきましょう。
先に結論を述べておきますと、メリットは以下の2点です。
- プライベートプロパティ/メソッドの実現と保持
- ユーザーがカスタマイズ可能な「 高階関数」として使用することができる
それぞれ詳しく見ていきましょう。
プライベートプロパティ/メソッドの実現と保持
クロージャを使用する必要がある場合というのは、ほとんどがこのためなんじゃないでしょうか。
実際にコードを見るのが早いと思うので、いくつか例を挙げてみます。
例1 : パスワードを 外部から変更されない変数に保存する
var getPass = (function(){
var passCode = "1bGf(eaQW&8"; // 外部から変更できないようにしたい
//以下をクロージャーとしてgetPassに渡す
return function () {
return passCode;
};
})();
getPass(); // パスワードの取得
この例では getPassがクロージャとなり、環境としてパスワードの文字列 " 1bGf(eaQW&8" を記憶しています。
こうすることで、 getPass()を呼び出すことでいつでもパスワードを取得できる上に、他の場所で passCodeという同じ名前の変数が使用されても上書きされる心配がありません。
*このように、エンクロージャを即時関数にし、そのまま変数に代入してクロージャを生成するという方法がよく使用されているようです。
例2 : プライベートなカウンター変数を実装する
var counter = (function () {
var count = 0; //プライベートなカウンター変数
return function () {
count ++; //呼び出されると更新
return count;
};
}());
counter(); //1
counter(); //2
counter.count; //->undefined(外部からカウンター変数にはアクセスできない)
例1 ではただ単に文字列を記憶させるだけでしたが、今回は少し違いますね。
実は、 クロージャが記憶している環境内の変数はクロージャ自身によって更新することができます。
この性質を利用して、上記の 例2 ではクロージャを呼び出す度に自身が記憶している 変数countを更新することで、自身が呼び出された回数を記憶しています。
外部から直接 変数countへアクセスすることはできないので、より安全なカウント機能を実装できるのです。
*ちなみに、カウンターの出力を0から始めたい場合はクロージャの中身となる部分を以下のように変更します。
return function () {
return count++;
};
例3 : 初回呼び出し時とそれ以降でメッセージを出しわける
var clickMessage = (function (){
var isClicked = false;
return function() {
if (isClicked) {
return "すでにクリックしていますね~。";
}
isClicked = true;
return "初クリック!";
}
})();
clickMessage (); // 初クリック!
clickMessage (); // すでにクリックしていますね~。
このような使い方もできます。これを応用すれば、動作を2回目以降で制限する、というようなこともできますね。
ユーザーがカスタマイズ可能な「高階関数」として使用することができる
クロージャを使用するとユーザが引数を与えてカスタマイズ可能な自由度の高い関数を生成することができます。
このような関数を「 高階関数」と呼ぶそうです。
クロージャのメリットというよりは、クロージャの応用と呼ぶべきかもしれません。
こちらも、例を見てみましょう
例 : 円の面積を求めるときに、使用する円周率をユーザーによってカスタマイズ可能にした関数
function circle_area_func(pi){
//円の面積を求める関数を返す
function circle_area(radius){
return pi * radius * radius;
}
return circle_area;
}
//円周率を 3 に設定した場合の面積を計算する関数を生成
caFunc1 = circle_area_func(3)
//次に、円周率を3.14に設定した場合の関数を生成
caFunc2 = circle_area_func(3.14)
//上記で作成した2つの関数に、半径=2 を引数に与えて、演算結果を取得
caFunc1(2); //->12
caFunc2(2); //->12.56
このように、 エンクロージャに引数を渡すことで、処理内容の異なるクロージャを生成することが可能になります。
ゆとり教育用に関数を分けるようなことだってできちゃうわけです。笑(私はゆとりなので円周率は3でした)
クロージャを使用する際の注意点
便利なクロージャにも注意すべき点があるようですので、最後にそのデメリットもまとめておきたいと思います。
注意すべき点は以下。
・メモリリークによる実行速度の低下
むやみにクロージャを多用すると、メモリリークの温床となってしまうそうです。
Google JavaScript Style Guide 和訳 のページにも、以下のように記述されています。
使っても良い. ただし慎重に.
クロージャは JavaScript の中でも最も便利でよく見る機能です.
ただし一点注意すべき点は, クロージャはその閉じたスコープへのポインタを保持しているという点です. そのため, クロージャを DOM 要素に付加すると循環参照が発生する可能性があり, メモリリークの原因となります.
構造に注意しておかないと、使用の有無に関わらず変数への参照を保持し続けてしまう上に循環参照が発生してしまうことで、メモリリークの原因となるようです。
ここまで時間をかけてクロージャを理解してきましたが、実際に使用するのは本当に必要な時だけにしておいたほうがよさそうです。
この辺に関してはもう少し勉強してから後日まとめてみようかなと思います。とりあえずはメモリリークが発生する危険があるんだなぁ、という認識を頭の隅にでも置いておくとします。
「【JavaScript】メモリの浪費を避けるコーディング」という記事で、 GC(ガベージコレクション) や循環参照について詳しく説明されていますので、さらに詳しく踏み込みたい方は読んでみてください。
おわりに
JavaScriptのスコープ・レキシカルスコープに触れてから、クロージャについてまとめてみました。
理解するのに時間がかかってしまいましたが、前からクロージャのことは気になっていたので頭の中がかなりスッキリしました。
クロージャについてはサイトによって様々な説明がなされていますので、色んな記事を読み比べて自分なりの理解を深めてみてください!