JavaScript

Closure

23 May 2014

 クロージャの使用 (MDNより)

function init() {
  var name = "Mozilla"; // ローカル変数

  function displayName() { // initの内部関数定義
    alert(name); // この関数の外側で定義された name は利用できる
  }
  displayName(); // 内部関数の呼び出し
};

function makeFunc() {
  var name = "Mozilla"; // ローカル変数

  function displayName() { // initの内部関数定義
    alert(name);
  }
  return displayName; // 内部関数が makeFuncから返される
}

var myFunc = makeFunc(); // makeFunc()の返り値をmyFuncへ代入

myFunc(); // 関数の実行

前の例とは異なる点は、内部関数 displayName() がそれが実行される前に外部関数から返されているという事です。

通常は、関数内部のローカル変数はその関数が実行されている間だけ存在します。 一旦 makeFunc() の実行が完了したら、name 変数はもう必要とされなくなると考えた方が筋は通っています。 ただこのコードが期待したとおりに動くという事は、これは明らかに事実と異なります。

この謎に対する解答は、myFunc が __クロージャ__ になったということです。 クロージャは関数とその関数が作られた環境という 2 つのものが一体となった特殊なオブジェクトです。 この環境は、クロージャが作られた時点でスコープ内部にあったあらゆる変数によって構成されています。 この場合、myFunc はそれが作られた時に存在していた displayName 関数と 文字列 "Mozilla" を取り込んだクロージャです。

ここにもう少し面白い例があります。makeAdder 関数です。

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
};

var add5 = makeAdder(5);
var add10 = makeAdder(10);

alert(add5(2)); // 7 と表示される
alert(add10(2)); // 12 と表示される

この例では、1 つの引数 x を取り、新しい関数を返す makeAdder(x) 関数を定義しています。 返される関数は 1 つの引数 y を取り、x と y の和を返します。

要するに、makeAdderは関数ファクトリです。 これは与えられた引数に特定の値を足す関数を作ります。 上の例では関数ファクトリを使って 2 つの新しい関数を作成しています。 1 つは引数に 5 を加えるもので、もう 1 つは 10 を加えるものです。

add5 と add10 は両方ともクロージャです。 両者は同じ関数本体の定義を共有していますが、保有している環境は異なります。 add5 の環境では x は 5 で、add10 の環境では x は 10 です。

実用的なクロージャ

クロージャは実際の役に立つのでしょうか?

クロージャを使うと、データ (環境) をそれを操作する関数と結びつける事が出来ます。

これはオブジェクトを使ってデータ (オブジェクトのプロパティ) を 1 つかそれ以上のメソッドと結びつける事が出来るオブジェクト指向プログラミングと明らかに類似しています。

したがって、メソッドを 1 つだけ持つオブジェクトを使いたくなるような状況ならば、どんな時でもクロージャを使う事ができます。

JavaScript ではこのような状況はよくあります。 私たちが書く JavaScript のコードは大半がイベントベースです。 つまり、ある動作を定義し、それを click や keypress といったユーザーによって引き起こされるイベントに取り付けます。 私たちのコードの多くはコールバック、すなわちイベントに反応して実行される単独の関数として取り付けられます。

実例を挙げましょう。あるページにそのページのテキストの大きさを調整するためのボタンを追加しようとしているとします。 1 つの方法として、まず body 要素の font-size をピクセル数で指定して、そのページ内の (見出しなどの) 他の要素のサイズを相対単位 em で設定します。

body {
  font-family: Helvetica, Aria, sans-serif;
  font-size: 12px;
}

h1 { font-size: 1.5em; }
h2 { font-size: 1.2em; }

これから作る対話式のテキストサイズ調整ボタンは、body 要素の font-size プロパティを変更し、その変更は相対単位によってページ上のほかの要素にも適用されます。

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  }
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

size12、size14、size16 はそれぞれ body のテキストサイズを 12、14、16 px に変更する関数になっています。これらは以下のようにしてボタン (この場合はリンク) に取り付けられます。

function setupButtons() {
  document.getElementById('size-12').onclick = makeSizer(12);
  document.getElementById('size-14').onclick = makeSizer(14);
  document.getElementById('size-16').onclick = makeSizer(16);
}
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>

クロージャでプライベートメソッドを模倣する

Java などの言語ではプライベートなメソッドを宣言することが出来ます。 これは同じクラス内にあるほかのメソッドからのみ呼び出せるメソッドのことです。

JavaScript にはこういった機能は組み込まれていませんが、 クロージャを使うとプライベートメソッドを模倣する事ができます。

プライベートメソッドはコードへのアクセスを制限するのに役立つだけではなく、 コードのパブリックインターフェースが不要なメソッドでいっぱいになるのを防ぐため、 グローバル名前空間を管理するのに非常に有効です。

クロージャを使って、プライベートな関数と変数にアクセスできるパブリック関数を定義するにはこのようにします。

var Counter = (function() { // 無名関数の定義
  // 共有環境
  var privateCounter = 0; // プライベート変数

  function changeBy(val) { // プライベート関数
    privateCounter += val;
  }

  // 下記の3つの関数は上記の環境を共有している
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();

alert(Counter.value()); /* 0 と表示される */
Counter.increment();
Counter.increment();
alert(Counter.value()); /* 2 と表示される */
Counter.decrement();
alert(Counter.value()); /* 1 と表示される */

ここでは色々な事が行われています。

前の例ではクロージャがそれぞれ独自の環境を持っていましたが、この例では環境が 1 つだけ作成され、その環境は Counter.increment、Counter.decrement、Counter.value という 3 つの関数によって共有されています。

この共有環境は、定義されるとすぐに実行される無名関数の本文で作成されています。

この環境は変数 privateCounter と関数 changeBy という 2 つのプライベートアイテムを含んでいます。

これらはどちらも無名関数の外側からは直接アクセス出来ません。

その代わり、この無名ラッパ関数から返される 3 つのパブリック関数からはアクセスできます。

これら 3 つのパブリック関数は同じ環境を共有するクロージャです。

JavaScript のレキシカルスコーピングにより、これらの関数はそれぞれが変数 privateCounter と関数 changeBy にアクセスできます。

このようにしてクロージャを使うと、普通はオブジェクト指向プログラミングに付き物のいくつかの利点、具体的にはデータの隠蔽やカプセル化が利用できるようになります。

よくある間違い: ループ内でクロージャを作成する

JavaScript 1.7 で let キーワードが導入される前までは、ループの内部でクロージャが作成された時にある問題がよく起こっていました。次のような例を考えます。

<p id="help">ここにヘルプが表示されます</p>
<p>Eメール: <input type="text" id="email" name="email"></p>
<p>名前: <input type="text" id="name" name="name"></p>
<p>年齢: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
    {'id': 'email', 'help': 'あなたのEメールアドレス'},
    {'id': 'name', 'help': 'あなたのフルネーム'},
    {'id': 'age', 'help': 'あなたの年齢 (17 歳以上)'}
  ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

ここにヘルプが表示されます

Eメール:

名前:

年齢:

配列 helpText は 3 つのヘルプを定義しており、それぞれがドキュメント内の入力フィールドの ID と関連付けられています。

ループがこれらの定義を巡回して、それぞれの入力フィールドの onfocus イベントをそれに関連付けられたヘルプを表示するメソッドと結び付けています。

このコードを実行してみると、期待したとおりには動かないのが判ります。

どのフィールドにフォーカスしても、表示されるのは年齢についてのメッセージです。

こうなる理由は、onfocus に代入された関数がクロージャだからです。

このクロージャは、関数定義と、 setupHelp 関数のスコープから捕捉された環境から成っています。

クロージャは 3 つ作られましたが、これらはみな 1 つの同じ環境を共有しています。

onfocus コールバックが実行される時にはループはすべて終了しており、変数 item (3 つのクロージャ全てに共有されている) は helpText リストの最後の項目を示したままにされています。

こういった場合の解決策の 1 つとして、より多くのクロージャを使う方法があります。

具体的には、前に述べたような関数ファクトリを使います。

function showHelp1(help) {
  document.getElementById('help1').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp1(help);
  }
}

function setupHelp1() {
  var helpText = [
    {'id': 'email1', 'help': 'あなたのEメールアドレス'},
    {'id': 'name1', 'help': 'あなたのフルネーム'},
    {'id': 'age1', 'help': 'あなたの年齢 (17 歳以上)'}
  ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp1();

ここにヘルプが表示されます

Eメール:

名前:

年齢:

これは期待通り動きます。

全てのコールバックが 1 つの環境を共有するのではなく、makeHelpCallback 関数がそれぞれに対して新しい環境を作っており、そこでは help が配列 helpText の対応する文字列を参照しています。

JavaScript 1.7 以上を使用しているなら、ブロックレベルのスコープを持つ変数を作成できる let キーワードを使ってこの問題を解決できます。

for (var i = 0; i < helpText.length; i++) {
  let item = helpText[i];

  document.getElementById(item.id).onfocus = function() {
    showHelp(item.help);
  }
}

let キーワードによって変数 item がブロックレベルスコープで作成されるため、for ループによる反復のたびに新しい参照が作られます。

つまりそれぞれのクロージャに対して別個の変数が捕捉されるため、環境の共有によって起こる問題が解決します。

パフォーマンスへの配慮

あるタスクを実行する時、クロージャが必要とされていないのにいたずらに関数を他の関数の中に作成するのは、スクリプトのパフォーマンスに悪影響を及ぼすのであまり賢いやり方ではありません。

例えば、新しくオブジェクト/クラスを作成する時、一般的にメソッドはオブジェクトのコンストラクタの中で定義するのではなく、オブジェクトのプロトタイプに結びつけるべきです。

コンストラクタの中で定義してしまうと、コンストラクタが呼び出されるたびに (つまりオブジェクトが作成されるたびに) メソッドが再代入されてしまうことになるからです。

次の実践的ではない例証のためのケースを考えます。

function MyObject(name, message) {
  this.name = String(name);

  this.message = String(message);

  this.getName = function() {
    return this.name;
  }

  this.getMessage = function() {
    return this.message;
  }
}

上のコードではクロージャを使う事によって得るものが何もないので、再構成するべきです。

function MyObject(name, message) {
  this.name = String(name);
  this.message = String(message);
}

MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};

もしくは、

function MyObject(name, message) {
  this.name = String(name);
  this.message = String(message);
}

MyObject.prototype.getName = function() {
  return this.name;
}

MyObject.prototype.getMessage = function() {
  return this.message;
}

上の 2 つの例では、プロトタイプが継承されて全てのオブジェクトによって共有されるため、オブジェクトが作成されるたびにメソッドが定義されずに済みます。