JavaScriptには「即時関数」という構文があります。即時関数は関数を定義すると同時に実行するための構文で、この即時関数を使ってコードを書いたことのある方も多いのではないかと思います。
JavaScriptに慣れている方にとっては「何を今さら」といった書き出しかもしれませんが、私はこの即時関数を初めて知った時、その必要性がイマイチ見出せませんでした。それは、「関数を定義と同時に実行するのは分かるけど、別に普通に関数を定義して、その関数を呼び出せばいいじゃん」って思ったからです。
確かに、一度しか使われないような関数をいちいち名前付きで定義してそれを呼び出すというコードは冗長的かもしれません。そのような場合は即時関数を使った方がよりスマートなコードになるでしょう。ですが、それだったらそもそも関数にしなくても良いのでは?などと考えていたので、私は即時関数を「再利用しない関数を実行する場合のコード量を減らすための手段」程度にしか考えていませんでした。
しかしオープンソースのコードを見ると、結構至る所で即時関数が使われていたりします。
私から見たらかなり「メリットの薄い」即時関数がなぜ世間では当たり前の様に使われているのかが理解できなかったのですが、自分で実際に即時関数を使い始めてみてようやくその「使いどころ」的なものが分かってきました。
今回は、この「即時関数」について、少し掘り下げてまとめてみました。
即時関数の構文
まずは簡単なおさらいとして即時関数の構文を見ておきましょう。以下に即時関数の書き方(2パターン)を示します。
//即時関数の構文その1 (function () { //関数の中身・・・ }()); //即時関数の構文その2 (function () { //関数の中身・・・ })();
一見両者の違いが分かりづらいですが、最後の括弧の位置が異なります。どちらも即時関数の構文としては問題ありませんが、JSLintでは前者の書き方が推奨されます。(後者の書き方だとJSLintでは「Move the invocation into the parens that contain the function.」と警告されます。)
また、即時関数は定義の後すぐに実行され再利用もされないため、関数名は不要です。
即時関数に引数を渡す場合は、以下の様にして渡すことが可能です。
//構文1の場合 (function (param1, param2, ...) { //関数の中身・・・ }('hoge', 'fuga', ...)); //構文2の場合 (function (param1, param2, ...) { //関数の中身・・・ })('hoge', 'fuga', ...);
また、即時関数も通常の関数同様、戻り値を持たせることもできます。
var result = (function (param1, param2) { return param1 + param2; }(1, 2)); console.log(result); //3が出力される。
即時関数はスコープを汚染せずに新たなスコープを作成するための唯一の手段
JavaScriptには他の言語にあるようなブロックスコープがありません。あるのはグローバルスコープと関数スコープだけです。
言い換えると、関数は任意にスコープを作るための唯一の手段だと言えます。即時関数も関数ですのでスコープを提供します。
関数スコープの中でvarを使って定義された変数は関数の中でローカルな変数になるので、関数の外側の変数を上書きしたりすることはありません。
例えば、コードの中にある一連の処理があるとします。これらの処理がいくつかの一時変数を使用する場合、これらの変数を全てグローバル変数にしてしまうのはアンチパターンです。このようなことを繰り返していると、コードが大きくなるにつれグローバルスコープが不要な変数名であっという間に埋め尽くされてしまい、コーディングがしづらくなるだけではなく、予期せぬトラブルの元にもなります。変数の有効範囲は可能な限り局所的にすべきです。
次のコードは極端な例ではありますが、変数 result の値を求めるために、一時変数 a, b を利用していることを示すコードです。
//結果を格納する変数 var result = 0, //結果を求めるための一時変数 a = 1, b = 2; result = a + b; console.log(result); //「3」が出力される
上記のコードは期待した通りに動作しますが問題があります。それは、結果を求めるための一時変数(a, b)をグローバルスコープ上に定義してしまっていることです。そのため、もし上記コードよりも前の部分で同じ変数名が定義されていた場合、その変数を上書きしてしまうことになり、予期せぬ結果をもたらす場合があります。
次のコード例は、関数を使って上記の一時変数(a, b)をスコープの中に閉じ込めるようにした場合の例です。
//結果を格納する変数 var result = 0, //結果を求める関数 calc = function () { //結果を求めるための一時変数 var a = 1, b = 2; return a + b; }; result = calc(); console.log(result); //「3」が出力される
このコードは最初の例よりかは幾分マシです。結果を求めるための一時変数(a, b)が関数スコープの中に閉じ込められているので、関数外部の変数を上書きしてしまうようなことは有りません。また、関数の実行後に一時変数がグローバルスコープ上に残り続けるような事もありません。
しかしこの場合も結局「calc」という関数を格納するための変数をグローバルスコープ上に定義しているので、最初のコード例と同じ問題が発生する可能性があるため、あまり良い方法であるとは言えません。calc関数が再利用されないようなものであればなおさらです。
そこで、このコードを即時関数を使って書き換えたものが次のコードです。
//結果を格納する変数 var result = 0; result = (function () { //結果を求めるための一時変数 var a = 1, b = 2; return a + b; }()); console.log(result); //「3」が出力される。
コード例2の方ではcalcという関数を定義してそれを実行していましたが、この部分を即時関数に置き換えることによって calc という無駄な変数を宣言する必要がなくなります。この例では、結果を格納する変数 result のみがグローバル変数であり、それ以外に新たに作成されたグローバル変数は有りません。
コード例2とコード例3はどちらも関数を使い、新たなスコープを作成してその中に一時変数を閉じ込めていることには変わりないのですが、コード例2の方は関数そのものを格納するための変数をグローバルスコープ上に作成しているため、グローバルスコープを汚染してしまっています。これに対し、コード例3の方ではグローバルスコープを全く汚染せずに一時変数を関数スコープの中に閉じ込めることが出来ています。
つまり、即時関数は現在のスコープを汚染せずに新たなスコープを作成することができるということです。また、このようなことができるのはJavaScriptでは即時関数だけです。
今回の例の様に、一連の処理を即時関数で包み一時的なスコープの中で実行することは、プログラムをスコープ上のサンドボックス内で実行することになるため、スコープの外側へ影響を与えてしまうことを防ぎます。即時関数が必要とされるのはこのためです。
即時関数は確かに「関数」ではあるのですが、通常その用途は名前付き関数とは異なります。
一般的な名前付き関数が再利用を前提とした一連の処理を定義するために使われるのに対し、即時関数は再利用されない一連の処理を新たなスコープに閉じ込めることを目的として使われることが多いと考えられます。
スコープという観点から見たら、どちらも新たなスコープの中に処理を閉じ込めているのですが、コードが再利用されないのであれば、現在のスコープを汚染しない即時関数を使った方が良いでしょう。
名前付き関数 : 再利用を前提とした一連の処理を定義する
即時関数 : 再利用されない一連の処理を新たなスコープで包み込む
即時関数が使われるケース
即時関数を「現在のスコープを汚染せずに新たなスコープを作成するためのテクニック」と捉えるといくらでも使いどころがありそうですが、むやみに即時関数を使うのはかえってコードの可読性を低下させてしまうことにつながるのではないかと思います。(即時関数内部のコード量が増えてくると、関数の範囲を確認するために何度も画面をスクロールしなければならないはめになるからです。)
したがって、即時関数を使う場合はある程度使いどころを見極めたうえで使っていくことが大切なのではないかと考えます。
以下は一般的に即時関数がよく使われるケースの一例です。
■ページの初期化
以下はページ上のボタンをクリックした時に現在日時を表示するコード例です。
<html> <body> <span id="date_label"></span> <button id="show_button">現在日時を表示</button> <script src="init.js"></script> </body> </html>
//初期化処理 (function () { 'use strict'; var label = document.getElementById('date_label'), button = document.getElementById('show_button'); button.onclick = function () { var now = new Date(); label.innerText = now; }; }());
ページが読み込まれた際にページの初期化(イベントハンドラの設定)を行っていますが、いくつかの一時変数を定義しています。これらの一時変数は初期化完了後にはもはや不要なので、グローバル変数として定義するのは好ましくありません。そこで、初期化コード全体を即時関数で包むことで初期化完了後に一時変数などの不要な副産物を残さないようにしています。
■機能判定
JavaScriptプログラムの初期化処理において、プログラムの実行中に変化しない情報(ブラウザ種別、サポートしている機能 など)を事前に取得しておくケースは良くあるかと思います。
以下の例は閲覧中のブラウザの種別(ベンダー)を取得するためのコードです。(※大抵の場合、この手の処理はサードパーティー製のJavaScriptライブラリが行ってくれたりするので、自前で実装することはあまりないかもしれませんが。。)
var VENDER_PRIFIX = '', ua = navigator.userAgent; if (ua.indexOf('Opera') != -1) { VENDER_PRIFIX = 'O'; } else if (ua.indexOf('MSIE') != -1) { VENDER_PRIFIX = 'ms'; } else if (ua.indexOf('WebKit') != -1) { VENDER_PRIFIX = 'webkit'; } else if (navigator.product == 'Gecko') { VENDER_PRIFIX = 'Moz'; } else { VENDER_PRIFIX = ''; }
上記のコードを見ると、ブラウザのベンダー文字列を格納する変数 VENDER_PRIFIX と、結果を得るための一時変数 ua をそれぞれグローバル変数として定義しています。しかしこの一時変数は VENDER_PRIFIX の値が確定した後は不要なので、グローバル変数として定義するのは好ましくありません。
そこで、ベンダー判定処理全体を即時関数で包み、即時関数の実行結果を VENDER_PRIFIX に格納するようにした例が次のコードです。
このようにすれば、結果を得た後に一時変数が残るようなことは有りません。
var VENDER_PRIFIX = (function () { var ua = navigator.userAgent; if (ua.indexOf('Opera') != -1) { return 'O'; } else if (ua.indexOf('MSIE') != -1) { return 'ms'; } else if (ua.indexOf('WebKit') != -1) { return 'webkit'; } else if (navigator.product == 'Gecko') { return 'Moz'; } else { return ''; } }());
■プライベートプロパティ/メソッドの定義
JavaScriptには private や protected といった「アクセス修飾子」はありません。そのためオブジェクト内に定義されたプロパティ/メソッドは、基本的には全てパブリック(public)なものとして扱われます。しかし、即時関数や、クロージャといったJavaScriptの基本テクニックを利用することで簡単にプライベートなプロパティ/メソッドを定義することが可能です。
例えば、increment、decrementという2つのメソッドをもった単純なカウンターオブジェクトを作成するとします。incrementメソッドは現在のカウントに1を加え出力し、decrementメソッドは現在のカウントから1を引いて出力します。また、現在のカウントを保持するために内部変数 count を持たせています。この内部変数はincrement/decrementメソッドからのみアクセスを許可し、オブジェクトの外部からのアクセスを拒否するものとします。(つまり、プライベート変数にしたい)
以下に、このような機能を持ったオブジェクトを即時関数を使用せずにオブジェクトリテラルを用いて作成した例を示します。
var counter = { //プライベートにしたいプロパティ count: 0, //加算メソッド increment: function () { this.count += 1; console.log(this.count); }, //減算メソッド decrement: function () { this.count -= 1; console.log(this.count); } }; counter.increment(); //1が出力される counter.increment(); //2が出力される counter.decrement(); //1が出力される console.log(counter.count); //※countプロパティを直接参照/書き換えが出来てしまう
上記のコードは見かけ上は期待通りに動作しますが、ひとつ問題があります。それは、現在のカウントを保持するための内部変数(count)がパブリックプロパティになってしまっているため、オブジェクトの外部から容易に参照、上書きが出来てしまうということです。
変数 count をプライベートプロパティとして実装したいところですが、JavaScriptにはそのような構文が存在せず、オブジェクトリテラル表記で定義されたプロパティ/メソッドは全てパブリックになってしまいます。
そこで、このオブジェクトを即時関数(+オブジェクトリテラル)で作成するようにした例が次のコードです。
var counter = (function () { //プライベートにしたいプロパティ var count = 0; return { //加算メソッド increment: function () { count += 1; console.log(count); }, //減算メソッド decrement: function () { count -= 1; console.log(count); } }; }()); counter.increment(); //1が出力される counter.increment(); //2が出力される counter.decrement(); //1が出力される console.log(counter.count); //※undefined
先ほどのcount プロパティに相当するものを関数内のローカル変数として定義し、increment/decrementメソッドをオブジェクトリテラルを使って定義しています。
上記のコード例では、変数 counter は increment、decrement という2つのメソッドを持ったオブジェクトとなり、coutプロパティは存在しません。これは、オブジェクトの外部から内部変数 count にアクセスする手段がないことを意味します。ですが、内部変数 count はクロージャとして存在し続けるので、現在のカウントを保持させることが可能です。
JavaScriptには変数のプライバシーを定義するための構文はありませんが、このように即時関数やクロージャといったテクニックを利用すれば比較的簡単にプライバシーを実現することができます。
まとめ
即時関数は、名前を持たない関数をその場で実行しているに過ぎないため、本質的には通常の名前付き関数と同じです。そのため、「即時関数を使わなければ実現できない処理」といったものはあり得ません。それらは即時関数でなくとも通常の名前付き関数で処理することは可能です。
しかし即時関数は名前を持たないため、現在のスコープを汚染せずに新しいスコープを作成することができます。このことが通常の名前付き関数との違いであり、即時関数ならではの用途を生み出します。
- 即時関数は本質的には通常の名前付き関数と同じ。つまり「即時関数を使わなければ実現できない処理」といったものはあり得ない
- 即時関数は、現在のスコープを汚染せずに新たなスコープを作成するための唯一の手段
- 即時関数の主な用途は、再利用されない一連の処理を新たなスコープで包み、プログラムをスコープ上のサンドボックス内で実行すること
即時関数を使って、プログラムのスコープを適切に制御することができれば、コードの見通しやメンテナンス性を改善するすることに役立ちます。