JavaScriptのコードを読んでいると、たまに出てくる (function(){ ... })(); という謎のカッコだらけの書き方。初めて見たときは「なんでこんなにカッコで囲むの?」と首をかしげた方も多いのではないでしょうか。これが今回のテーマであるIIFE(即時実行関数式)です。今回はIIFEの基本の書き方から、なぜ使うのか、アロー関数やasyncでの書き方、ハマりやすい落とし穴、そしてES Modulesが当たり前になった今でも出番がある場面まで、まとめてご紹介します。
IIFE(即時実行関数式)とは
IIFEは「Immediately Invoked Function Expression」の略で、日本語では「即時実行関数式」または「即時関数」と呼ばれます。読み方は「イッフィー」が一般的です。その名のとおり、関数を定義したその場ですぐに実行してしまう書き方のことですね。「IIFE」という呼称は、jQueryプラグインなどで有名なBen Alman氏が2010年に自身のブログで使用を提案したことで広まりました。書き方そのものはそれ以前から使われていて、古い記事では「自己実行無名関数」などの呼び方も見かけます。
ポイントは名前に入っている「式(Expression)」という部分です。JavaScriptの関数には「関数宣言」と「関数式」の2種類があり、IIFEとして使うのは関数式のほうです。関数式は無名でも名前付きでもOKですが、関数宣言とは構文上の扱いが違うため、関数式として解釈される形にしてから呼び出す必要があります。この違いを押さえておくと、あの不思議なカッコの意味が一気に分かるようになります。逆にここを曖昧にしたまま書き方だけ覚えると、ちょっと形を変えたときにSyntaxErrorが出て「なんで?」となりがちです。
基本の書き方
まずは一番オーソドックスな書き方を見てみましょう。無名関数をカッコ(グループ化演算子)で囲み、その直後に関数呼び出しの () を付けるだけです。
(function () {
console.log('IIFE が実行されました');
})();前半の (function () { ... }) で関数式を作り、後半の () でそれを呼び出しています。つまり「関数を作る」と「関数を実行する」を1行(1文)で同時にやっている、というのがIIFEの正体です。分解してみると意外とシンプルですよね。単純な例なら、名前を付けた関数を作って次の行で呼んでも結果は同じです。ただしIIFEの場合は関数名が外に公開されず、どこからも参照されないまま実行が終われば用済みになる、というスコープ上の違いがあります。
なぜカッコで囲む必要があるのか
カッコを外して function () { ... }(); と書くと、ブラウザは「SyntaxError」を出して止まってしまいます。行頭に function キーワードがあると、JavaScriptエンジンはそれを「関数宣言」として解釈しようとするからです。関数宣言には名前が必須ですし、宣言の直後に () を付けて呼び出すこともできません。
そこで外側をカッコで囲むことで「これは宣言ではなく式ですよ」とエンジンに伝えているわけです。カッコの中には式しか書けないというルールを利用した、ちょっとしたテクニックですね。ちなみにカッコの位置は (function(){}()) のように呼び出し部分まで含めて囲んでもOKで、どちらを使うかは好みやコーディング規約次第です。チーム内で統一されていれば、どちらでも問題ありません。
IIFEを使う理由・メリット
書き方が分かったところで、「わざわざそんな書き方をする理由は?」という本題に入りましょう。IIFEが多用されていた背景には、かつてのJavaScriptに「ブロックスコープ」も「モジュール」も無かったという事情があります。var で宣言した変数は関数単位でしかスコープを区切れず、通常のscriptタグで読み込んだファイルのトップレベルに var や関数宣言を書くと、グローバルスコープに影響していました。
複数のscriptタグで読み込んだJSファイル同士は同じグローバル環境を共有しているので、別の人が書いたファイルと変数名や関数名がかぶると、知らないうちに値が上書きされてしまいます。デバッグする側からすると、これはもう地獄です。この問題を解決するために使われていたのが、関数スコープを利用して変数を閉じ込めるIIFEだったわけです。
グローバル汚染を防ぐ
IIFEの中で宣言した変数は、その関数の中だけで有効になります。初期化処理のように「一度実行すれば終わり」というコードをIIFEで囲んでおけば、処理用の変数がグローバルに残ることはありません。
(function () {
var count = 0;
var label = '初期化';
console.log(label, count);
})();
console.log(typeof count); // "undefined"関数の外から count を参照しても、存在しない扱いになっているのが分かりますね。ライブラリやプラグインを作るときに、利用者側のコードと名前が衝突しないよう、ファイル全体をIIFEで包むのは昔の定番パターンでした。従来のjQueryプラグインのソースでも、よく見られたパターンです。
クロージャでプライベート変数を作る
IIFEはクロージャと組み合わせることで、外部から直接書き換えられない「プライベートな変数」を作ることもできます。IIFEの戻り値として関数やオブジェクトを返すと、返された側は内部の変数を参照し続けられる一方、外からは触れない状態になります。
const counter = (function () {
let count = 0;
return {
increment() { return ++count; },
reset() { count = 0; }
};
})();
counter.increment(); // 1
counter.increment(); // 2
console.log(counter.count); // undefinedこれはいわゆる「モジュールパターン」と呼ばれる書き方で、ES Modulesが登場する前は、この仕組みで擬似的にモジュールを実現していました。count は外から見えないので、意図しない値の書き換えを防げるのが利点です。公開したいものだけを return で返す、という発想は今のexport/importにもそのまま通じる考え方ですね。
引数でグローバルオブジェクトを受け取る
IIFEは普通の関数なので、もちろん引数を渡すこともできます。よく見かけるのが、jQueryの $ を安全に使うためのパターンです。
(function ($) {
$('.btn').on('click', function () { /* ... */ });
})(jQuery);他のライブラリが $ を別の意味で使っていても、IIFEの内側では $ が確実にjQueryを指すようになります。CMSのように、どんなライブラリが一緒に読み込まれるか分からない環境では、地味ながらかなり頼りになる書き方です。同じ理由で window や document を引数で受け取り、圧縮時に短い変数名へ置き換えやすくする、というテクニックも昔はよく使われていました。
アロー関数・async・その他の書き方
ES2015(ES6)以降はアロー関数でもIIFEを書けるようになりました。また、async関数と組み合わせた書き方も実務ではよく使います。ここでは代表的なバリエーションをまとめて見ていきましょう。
アロー関数でのIIFE
(() => {
console.log('アロー関数のIIFE');
})();注意したいのはカッコの位置です。アロー関数でIIFEを書く場合は、関数全体をカッコで囲んでから、その外側に呼び出しの () を付けます。通常の関数式で使える (function () {}()) と同じ形をアロー関数に当てはめて (() => {}()) と書くと、SyntaxErrorになるので要注意です。また、アロー関数は自分自身の this を持たないので、IIFE内で this を使う古いコードをそのままアロー関数に置き換えると挙動が変わる点にも気を付けましょう。
async IIFE
トップレベルで await を使いたいけれど、実行環境の都合でtop-level awaitが使えない……というときに便利なのがasync IIFEです。
(async () => {
try {
const res = await fetch('/api/data.json');
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
}
const data = await res.json();
console.log(data);
} catch (error) {
console.error('データ取得に失敗しました', error);
}
})();ES Modulesであればtop-level awaitが使えますが、通常のscriptタグで読み込むファイルやCommonJS形式のNode.jsスクリプトでは使えません。ちょっとした検証用スクリプトや、既存の古い構成のサイトに非同期処理を追加するときに重宝します。なお、async IIFEは実行結果としてPromiseを返します。関数内で例外が起きるとそのPromiseはreject状態になり、誰も処理しなければ未処理の拒否(Unhandled Rejection)になってしまいます。上の例のようにtry/catchで囲むか、呼び出し時に .catch() を付けておきましょう。また、fetch() は404や500が返ってきただけではrejectされないので、res.ok の確認も入れています。
! や void を使った書き方
カッコ以外にも、単項演算子を前に置いて関数を「式」として扱わせる方法があります。
!function () { console.log('!を使ったIIFE'); }();
void function () { console.log('voidを使ったIIFE'); }();圧縮されたライブラリのコードで !function(){...}() を見かけたら、それはIIFEです。圧縮コードでは、文字数を少しでも削るためにこの形が使われることがあります。読みやすさを考えると、自分で書くコードでは素直にカッコを使ったほうがいいと思います。※個人の感想です。
IIFEでハマりやすい落とし穴
便利なIIFEですが、書き方に起因する典型的なエラーがいくつかあります。特にセミコロンを省略するスタイルで書いている方は要注意です。知らないと原因を突き止めるのにかなり時間を溶かすことになるので、ここで押さえておきましょう。
直前の行のセミコロン抜け
JavaScriptには自動セミコロン挿入(ASI)という仕組みがありますが、行頭が ( で始まる場合は前の行とつながって解釈されてしまいます。
const message = 'hello'
(function () {
console.log(message);
})();
// TypeError: "hello" is not a function上のコードは 'hello'(function(){...}) と解釈され、「文字列を関数として呼び出そうとした」というエラーになります。ファイルを結合するビルド処理で、前のファイルの末尾にセミコロンがなかった場合にも同じことが起こります。基本的な対策は、前の文をきちんとセミコロンで終えることです。そのうえで、IIFEの先頭に ;(function(){...})() とセミコロンを付けておく書き方が昔のライブラリでよく見られました。これは前のコードの末尾にセミコロンがなかった場合に備えた防御策ですね。
thisとstrictモード
通常の関数で書いたIIFEの中の this は、非strictモードではグローバルオブジェクト(ブラウザなら window)、strictモードでは undefined になります。IIFEの中で 'use strict' を宣言すると挙動が変わるので、this に依存したコードは避けるのが無難です。グローバルオブジェクトが必要なら、globalThis を直接参照するほうが意図が明確になります。ちなみにES Modulesは自動的にstrictモードで実行されるので、モジュール内に書いたIIFE(通常の関数式)の this も undefined になります。
今でもIIFEは必要?現代的な代替手段
ここまで読んで「でも今は let や const もあるし、ES Modulesもあるし、IIFEってもう要らないのでは?」と思った方もいるかもしれません。結論から言うと、日常的なアプリケーション開発でIIFEを書く機会はかなり減りました。ただ、完全に過去の遺物になったわけではなく、ちゃんと出番が残っている場面もあります。
ブロックスコープとES Modulesで代替できる
let と const はブロックスコープなので、単に変数を閉じ込めたいだけなら { ... } で囲むだけで済みます。
{
const temp = '一時的な値';
console.log(temp);
}
// ここでは temp は参照できないさらに <script type="module"> やES Modulesのファイルは、トップレベルの変数がそもそもグローバルに漏れません。import/exportで公開範囲も明示できるので、モジュールパターンとしてのIIFEはほぼ役目を終えたと言っていいでしょう。ただし、関数スコープの中で値を計算して戻り値として受け取る、といった使い方まで単なるブロックで置き換えられるわけではありません。ReactやVueなどのフレームワークでの一般的なコンポーネント開発なら、ビルドツールがモジュール単位でスコープを分けてくれるので、自分でIIFEを書く場面はかなり限られます。
それでもIIFEが活躍する場面
一方で、モジュールとして読み込めない環境では今でもIIFEが現役です。たとえばブックマークレットはURL欄に貼り付けて実行する1本のコードなので、閲覧中のページの変数と衝突しないようIIFEで包むのが定番です。CMSの管理画面からHTMLに直接scriptを埋め込むケースでも、IIFEで変数や関数を局所化しておけば名前の衝突を防ぎやすくなります。ただしDOM操作やイベントの重複登録など、スコープ以外が原因の干渉には別途対策が必要です。
Chrome拡張機能のコンテンツスクリプトでも、同じ実行環境でスクリプトを複数回実行する設計だと、トップレベルの const や let の再宣言が問題になることがあります。処理をIIFEで関数スコープに閉じ込めれば、こうした衝突を避けやすくなります。とはいえ実行環境や注入方法によって事情は異なるので、IIFEだけで二重実行の問題がすべて解決するわけではありません。また、esbuildやRollupなどのバンドラーには出力形式として「iife」が用意されていて、scriptタグでそのまま読み込める1ファイルを生成するときに使われています。自分で書かなくても、裏側では今もIIFEが使われているわけですね。
まとめ
IIFE(即時実行関数式)は、関数式を定義したその場で実行する書き方で、(function(){ ... })(); という形が基本です。外側のカッコは「これは関数宣言ではなく式です」とエンジンに伝えるためのもので、ここを理解しておくとアロー関数版や !function(){}() のような変形版を見ても戸惑わなくなります。
IIFEが広く使われていた理由は、グローバル汚染を防ぎ、クロージャでプライベートな変数を作るためでした。var しかなく、モジュールの仕組みもなかった時代には、ほぼ必須のテクニックだったと言っていいでしょう。jQueryの $ を引数で受け取るパターンや、セミコロンを先頭に付ける書き方も、この時代の工夫の名残です。
現在は let / const のブロックスコープやES Modulesのおかげで、自分でIIFEを書く機会は確実に減りました。新しくコードを書くなら、まずはモジュールやブロックスコープで解決できないかを考えるのがおすすめです。とはいえ、async IIFEはtop-level awaitが使えない環境で今も便利ですし、ブックマークレットやCMSへの埋め込み、Chrome拡張機能のコンテンツスクリプトなど「モジュールとして読み込めない場所」ではまだまだ現役です。
古いコードを読むときにも、新しいコードを書くときにも、IIFEの仕組みを知っておいて損はありません。「なぜこのカッコが必要なのか」が分かっていれば、レガシーなコードのリファクタリングもぐっと進めやすくなるはずです。それでは、カッコの数を数え間違えないように、よいJavaScriptライフを!
【参考文献】
- IIFE (即時実行関数式) – MDN Web Docs:https://developer.mozilla.org/ja/docs/Glossary/IIFE
- function 式 – JavaScript | MDN:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/function
- Immediately-Invoked Function Expression (IIFE) – Ben Alman:https://benalman.com/news/2010/11/immediately-invoked-function-expression/
- JavaScript モジュール – JavaScript | MDN:https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Modules
- esbuild – API(format):https://esbuild.github.io/api/#format