JavaScriptから即時関数を排除する

  • 11
    いいね
  • 0
    コメント

即時関数は関数式で関数を作ったら、即時に実行する関数のことです1。JavaScriptでは有名なテクニックの一つですが、他の言語ではほとんど見かけません。まず始めに、なぜ即時関数が必要だったのかを説明し、そこからいかにして即時関数を取り除くかを考えます。

JavaScriptに即時関数が必要な理由

ES52以前のJavaScriptには次のような問題がありました。

  • グローバルスコープか関数スコープの変数しかない。
  • モジュールベースではない。
  • 厳格モードへの切り替えが単なる文字列に過ぎない。

これを踏まえて、即時関数を使わざるを得ないところを見ていきます。

1. スクリプト全体を即時関数で囲む

どんなプログラミング言語であれ、一つのファイルに全てを書いていくことは現実的ではありません。いずれJavaScriptを複数のファイルに分割して書いていく必要があるでしょう。そのとき、グローバル汚染が大きな問題になってきます。

a.js
"use strict";
var x = 0;
function countA() {
    return x++;
}
b.js
"use strict";
var x = 0;
function countB() {
    return x++;
}
<script src="a.js"></script>
<script src="b.js"></script>
<script>
  console.log(countA());
  console.log(countA());
  console.log(countB());
  console.log(countB());
</script>

a.jsはb.jsは本来別々のものです。しかし、上の結果は0,1,2,3とcountA()countB()が相互に作用しあってしまいます。これは変数xがa.jsとb.jsで名前が被ってしまったため、共通で使用してしまっているからです。

そこで、即時関数3で囲みます。

a-iife.js
(function(global) {
    "use strict";
    var x = 0;
    function countA() {
        return x++;
    }
    global.countA = countA;
})(this);
b-iife.js
(function(global) {
    "use strict";
    var x = 0
    function countB() {
        return x++;
    }
    global.countB = countB;
})(this);

このようにすることで、0,1,2,3と正しくなります。即時関数内で"use strict";により厳格モードにしていることも注目してください。こうすることで、単純にa.jsとb.jsを結合しても、厳格モードは維持されますし、非厳格モードを汚すこともありません。

このようにJavaScriptでは本来ファイルを分割して作ることを考慮しておらず、単純な結合として考えています。そのため、グローバルでの汚染は複数のファイルを読み込んだときやファイルを結合したときに予期せぬ動作を起こすことになるでしょう。しかし、即時関数で囲むことでそのような汚染を回避できます。CoffeeScriptがデフォルトで即時関数で囲むようにトランスパイルするのもこの理由による物です。

2. スコープを作るために即時関数で囲む

scope.js
"use strict";
var x = 0;
var i = 0;
var sum = 0;
for (var i = 0; i < 10; i++) {
    var x = i * i;
    sum += x;
}
console.log(x);
console.log(i);
console.log(sum);

上を実行してみてください。81,10,285が表示されます。もし、CやJavaなど{}がブロックになる言語をしていることがあれば、この結果は奇妙に映ることでしょう。

varを使って宣言した変数は全て関数スコープ(トップであればグローバルスコープ)になります。for文の中であろうが外であろうが、xiは同じ物を指します。そのため、for文の後のxiは変更されてしまいました。上の例は小さいのですぐにわかりますが、多くの行数があるファイルであれば、最初に使っていた変数と同じ物を使っていると言うことに気付かないかも知れません。時には、重要なバグを引き起こす可能性があります。

scope-iife.js
"use strict";
var x = 0;
var i = 0;
var sum = 0;
(function() {
    for (var i = 0; i < 10; i++) {
        var x = i * i;
        sum += x;
    }
})()
console.log(x);
console.log(i);
console.log(sum);

今度はxiは変更されずに0のままです。このように即時関数で囲めば、その中でvarで宣言した変数はスコープを狭めることができます。

そもそも、適度に小さな関数に分割しておけば、気付かないという問題は起きないかも知れません。しかし、どんなに短くても見逃しというのはあり得ますし、CやJavaに慣れてしまっている人達にとっては、思わぬ落とし穴になってしまう可能性があります。

3. プライベートな変数のために即時関数で囲む

JavaScriptにはクロージャーという機能があります。これと即時関数を組み合わせれば、前回の呼び出しを記憶した関数というのを作れます。

private-iife.js
"use strict";
var count = (function() {
    var x = 0;
    return function() {
        return x++;
    };
})();
console.log(count());
console.log(count());

変数xはCでいうstaticなローカル変数です。このxに直接アクセスする方法はありません。しかし、count()は呼び出す度にxをインクリメントして、その値を返すため、xは関数そのものに記録されていると言えます。この方法を応用するとプライベートなクラス変数を作成できます。

private-field.js
"use strict";
var Counter = (function() {
    var x = 0;
    function Counter() {
        var y = 0;
        this.count = function() {
            x++;
            return y++;
        };
    };
    Counter.allCount = function() {
        return x;
    };
    return Counter;
})();
var a = new Counter;
var b = new Counter;
console.log("all: " + Counter.allCount());
console.log("a: " + a.count());
console.log("a: " + a.count());
console.log("all: " + Counter.allCount());
console.log("b: " + b.count());
console.log("b: " + b.count());
console.log("all: " + Counter.allCount());

xは即時関数の中でのみ有効な変数になり、いわゆるプライベートなクラス変数になります。なお、yはいわゆるプライベートなインスタンス変数ですが、yがある関数は即時に実行されるわけではありませんので、即時関数ではありません。

即時関数を排除する

ES64以降のJavaScriptでは、ES5以前の問題点を解決するための手段が提供されています。一つはブロックスコープの変数の導入、もう一つがモジュールの導入です。

1. スクリプト全体をモジュールで作る

最新ブラウザ5ではモジュールとして作ることができます。

conutA.js
var x = 0
export default function countA() {
    return x++;
}
countB.js
var x = 0
export default function countB() {
    return x++;
}
<script type="module">
  import countA from "./countA.js";
  import countB from "./countB.js";
  console.log(countA());
  console.log(countA());
  console.log(countB());
  console.log(countB());
</script>

モジュールとして読み込まれたcountA.jsとcountB.jsのそれぞれのxは別の物として扱われます。グローバルスコープがモジュールベースでは分離されているということです。そのため、カウントは別々に行われるため、もはや即時関数で囲む必要はありません。また、この方法の利点は、自動的に厳格モードになるということです。もはや"use strict";も書く必要はありません。

しかし、この方法には一つ問題があります。それは、対応するブラウザがまだ少ないと言うことです。2017年3月26日現在、正式に対応しているのはリリース直前状態のSafari 10.1のみです6。EdegとFirefoxの開発版では実装済みですが、デフォルトでは無効です。Chromeは開発版で実装すらされていません。各ブラウザの最新版で通常通りに使えるようになるには少なくとも今年いっぱいはかかることでしょう。ましてやIE11などという古く時代遅れのブラウザが対応する日は永遠に来ません。

レガシーなブラウザ向けにBabelとBrowserifyまたはWebpackを使って全体をES5にまとめる必要があります。HTMLをJavaScript部分を抜き出します。

all.js
import countA from "./countA.js";
import countB from "./countB.js";
console.log(countA());
console.log(countA());
console.log(countB());
console.log(countB());

Browserifyで変換する場合は、例えば次のようにします。npmのパッケージは適当にインストールしておく必要があります。

$ browserify all.js -o main.js -t [ babelify --presets [ latest ] ]

最後にHTMLです。

<script src="main.js"></script>

この場合もうまくxが分離されているでしょう。変換されたmain.jsを見ると一つの大きな即時関数になっており、また、countA.jsやcountB.jsの中身も関数の中にしまい込んでいて、それぞれのスコープが分離されるようになっています。

レガシーブラウザ向けにBabelによるES5への変換、ReactのためのJSX、依存したライブラリの結合、minifyによる圧縮等を考えると、JavaScript開発において現状では変換は必須であり、すでに何かしらタスクを組んでいるはずです。もし、BabelやBrowserify、Webpackを使っているのであれば、既にモジュールとして作る環境ができあがっていることですから、今すぐ、このモジュールの仕組みを利用できるということです。

2. スコープをブロック単位にする

ES6以降では、新たに二つの変数宣言が追加されました。それはletconstです。この二つがvarと大きく異なることはブロックスコープということです。scope.jsを書き換えて見ましょう。

scope-block.js
"use strict";
let x = 0;
let i = 0;
let sum = 0;
for (let i = 0; i < 10; i++) {
    let x = i * i;
    sum += x;
}
console.log(x);
console.log(i);
console.log(sum);

0,0,285とxiが書き換わって無い事が確認できると思います。let宣言された変数はブロックスコープであるため、トップにあるxiとfor文にあるxiはスコープが異なる別の変数と扱われます。これはCやJavaの変数と同じ動作です。letを使う限り、for文の中などで変数名が被っていて予期せぬ動作をしたと言うことがありません。また、単純にスコープを狭めたいのであれば{}で囲めばすぐにブロックスコープになります。

constは定数を表す変数です。最初に代入した後は再代入することができず、エラーになります。予期せぬ代入を防ぐために使うことができます。

3. プライベートな変数を作るためにSymbolとWeakMapを駆使する

private-iife.jsの例を二つ方法で書き換えて見ましょう。

private-symbol.js
"use strict";
const sym = Symbol('count');
const count = function count() {
    const x = count[sym] || 0;
    count[sym] = x + 1;
    return x;
};
console.log(count());
console.log(count());
private-weakmap.js
"use strict";
const wm = new WeakMap;
const count = function count() {
    const x = wm.get(count) || 0;
    wm.set(count, x + 1);
    return x;
};
console.log(count());
console.log(count());

さて、これは果たして本当にプライベートなのでしょうか?いいえ、プライベートではありません。普通の変数としてはxに入るカウントの値を取り出すことはできませんが、symwmがあれば取り出せてしまいますし、代入も可能です7。あえてするのであれば、ブロックを使った方法です。

private-block.js
"use strict";
let count;
{
    let x = 0;
    count = function() {
        return x++;
    };
}
console.log(count());
console.log(count());

即時関数を使った方法とあまり変わりありません。一つ言えるのはそもそもこのような方法は必要なのかと言うことです。関数を呼び出す度に状態に変化するのであれば、オブジェクトに状態を持たせてメソッドで呼び出すべきと考えるのが普通ではないでしょうか?ES6以降はクラス構文ができたため、オブジェクト指向も作りやすくなっています。

counter.js
"use strict";
class Counter {
    constructor() {
        this.x = 0;
    }
    count() {
        return this.x++;
    }
}
const c = new Counter;
console.log(c.count());
console.log(c.count());

再利用性を考えたら、こちらの方がいいでしょう。

さて、オブジェクト指向でクラスを使うのは良いとして、上の例ではインスタンス変数xはプライベートではありません。外から見えてしまいますし、書き換えることも可能です。他にもクラス変数をプライベートにしたいというのもありました。どうするのでしょうか?

結論から言うと、現状はまだできません。クラス構文にクラス変数やインスタンス変数を書くというPublic Class Fieldsとプライベートなインスタンス変数を書くというPrivate Fieldsは提案のStage2(ドラフト段階)の状態です。まだ仕様も確定していません。前者はBabelで実装済みなので使えないことはないのですが、後者はまだ未実装です(構文解析から変更が必要なようでペンディング中のようです)。また、プライベートなクラス変数というのものは提案すらされていません。

もし、Private Fieldsが正式採用された場合は次のように書けるでしょうが、2017年3月26日現在、この書き方を実装した環境はありませんし、Babelでの変換もできません。

private-counter.js
// 現在の所、これは動きません!!!
"use strict";
class Counter {
    #x = 0;
    count() {
        return #x++;
    }
}
const c = new Counter;
console.log(c.count());
console.log(c.count());

SymbolやWeakMapを使えばある程度隠蔽化は可能です。しかし、即時関数並に完璧とは言えません。ただ、考え方を変えると、JavaScriptはもともとプロパティは隠さないことを前提にしてきており、このままでもいいかもしれません。単なる命名規則、たとえば、_から始まるプロパティには直接アクセスしないようにすると言ったことだけでも十分かも知れません。

Pythonはインスタンス変数を完全に隠す方法は用意されていません。命名規則の慣習によってプライベートと見なすとしています。逆にRubyのインスタンス変数は常に隠れてますが、リファクタリングを使えばアクセスすることは可能です。そういうことを考えると、そもそも何が何でもプライベートにすることをこだわること自体があまり意味が無いのかも知れません。

まとめ

最後は締まらない結果になってしまいました。ただ、即時関数を使う例というのは1.の場合がほとんどであり、その次に2.になるでしょう。3.のクラス変数のプライベート化をそこまで頑張ってする必要があるのかは疑問に思うところです。

ES6以降は、モジュールとletconstで大分安全なスコープが確保されるようになりました。その部分だけでも、即時関数などと言う前時代的な、JavaScriptだけでしか使わないようなテクニックは捨てて、最新の方法を使った方が後々いいかと思います。


参考文献


  1. 無名、有名は問いません。関数式であっても有名関数は作成できます。ジェネレーター式やアロー関数でも同様に即時関数はつくれますが、今回は考慮していません。 

  2. ECMAScript5。JavaScriptの言語規格。 

  3. (function(){...}())派が多いようですが私は(function(){...})()派です。JSLintでエラー云々言う人達はESLintでwrap-iifeを好みに変えれば良いだけという事をたぶん知らないのかも知れません。 

  4. ECMAScript2015。バージョンは6ですが、毎年リリースするため年で表記するようになりました。2016=7が出ており、2017=8と続く予定です。 

  5. 各ブラウザのサポート状況はES6 module | Can I use...を参照してください。 

  6. コードのテストはSafari Technology Preview Release 26 (Safari 10.2, WebKit 12604.1.12)を使用しました。 

  7. モジュールベースで作っていれば、symwmはモジュールの中に綴じ込まれるので、取り出す手段はありません。