関数を理解すればクロージャは難しくない!

2015年08月31日

カテゴリー:

JavaScriptを学んでいく過程で必ず耳にする単語「クロージャ」

すごく難しいものではないと思うのですが、どことなくつかみどころがない、うまく説明できない、そんな存在ではないでしょうか? 私自身、正直なところクロージャについては「なんとなく使ってはいるが、使い方をうまく伝えられない」といった程度です。

これから先、クロージャについてモヤモヤしながら仕事をしていくのもどうかと思ったので、この機会に私もクロージャについて理解を深めるべく記事を書くことにしました。

クロージャは、それ自体が難しいというよりは、説明が難しいものだと思います。
そのため、ネットでクロージャのことを調べていくと、(もちろん大変参考になる情報も多いのですが)少し本質ではない部分について語られているものも多い気がします。

このエントリではクロージャの本質を極力シンプルに解説することを意識しました。サンプルコードは論点を明確にするためあまり実践向けの内容ではありませんがご容赦下さい。

まずは本質を理解することで、応用もしやすくなるでしょう。
この記事がクロージャ入門の一助となれば幸いです。


【余談】
クロージャに対する私の勝手な位置づけですが、クロージャをしっかりと理解できていればJavaScript初級者は卒業でしょう。JavaScriptエキスパートを目指す方にとって、クロージャは登竜門的な存在だと思うわけです。
なので、是非ともしっかりと理解しておきたいところですね。

※本エントリで説明する「関数」について

本記事では随所で関数について触れていますが、ここで対象としている関数は一般的な関数(関数宣言や関数式で定義された関数)を指しています。

evalやFunctionコンストラクタにより作成された関数はここでの説明に当てはまらないことが有るかもしませんので、予めご注意下さい。

「クロージャ」とは

クロージャに関する解説で「JavaScriptの関数はクロージャである」といったものをたまに見かけますが、この説明は少し語弊がありそうです。

私が参考にしたこちらのサイトではクロージャについて以下の様な説明をしています。

(中略)

myFunc(←関数名)がクロージャになったということです。クロージャは関数とその関数が作られた環境という 2 つのものが一体となった特殊なオブジェクトです。

クロージャ – JavaScript | MDN

上記の説明を読むと「関数=クロージャ」ではないことが分かります。詳しくはこの後解説しますが「関数はクロージャになり得る」という理解の方がイメージとしては正しいでしょう。

ちょっと回りくどい言い方になるかもしれませんが、クロージャの説明をあえて一文にまとめるならば、次の様な感じでしょうか。

JavaScriptの関数は、その関数が定義されたコンテキストとは異なるコンテキスト上にある変数に格納される時、その関数自身および関数の定義時のコンテキストが一体になった「クロージャ」という特殊なオブジェクトになる。

・・・分かりづらいっ!というか意味不明ですよね。言葉を絞り出したのですが私の国語力の問題でこんな文章になってしまいました。すみません。
上記の意味はこの後もう少し掘り下げて解説します。


といったことを書きつつも、実際のところ私はクロージャの定義をおぼえること自体にはあまり意味がないと考えています。クロージャを理解するというよりは、関数そのものの性質を理解することこそがクロージャの本質を知ることにつながると思うからです。というのも、クロージャは不思議な挙動を示しますが、こういった挙動は実は関数が持つ基本的なルールに厳密に従っているだけだからです。

クロージャの意味など知らなくても関数を理解すれば必然的にクロージャを使いこなせてしまうでしょう。
つまり、クロージャを知ることよりも、関数を知ることが大事なんです。
また、関数を理解することで、結果的にクロージャのことをよりシンプルに理解することができると考えています。

クロージャの本質

クロージャの本質に知るためには関数そのものの特性に触れる必要があります。
それでは、以下にクロージャを理解するために覚えておきたい3つの重要な関数の特性を順に紹介していきます。

1.JavaScriptの関数は定義時のコンテキストで実行される。

このことは、クロージャを理解するために最も重要な関数の特性です。そしてこれは何も特別なものではなく私達が普段書いているごく普通の関数がもつ特性で、JavaScriptの世界から見たら当たり前のことでもあります。

はっきり言ってしまうと、このことさえ理解できていればクロージャ云々を知らなくても、クロージャを使えているといっても良いくらいです。(これが先ほど「クロージャはシンプルに理解できる」と言った理由です)

クロージャの不思議な挙動について、ネットでは様々なサンプルコードを交えた解説がありますが、要はこれらクロージャ特有の挙動は上記に示した関数の特性に厳密に従っているだけなのです。

ここでいう「コンテキスト」とは、"その関数がおかれた環境"などと説明されたりもしますが、要は「その関数が参照可能な外部変数(=外側の関数のローカル変数とグローバル変数)」と考えて差し支えないでしょう。

そして、「定義時の」とありますが、これは関数の実行時ではなく、あくまでその関数が定義されたタイミングでのコンテキスト、ということです。このことをシンプルに表したものが以下のコードです。

var scope = 'global';

function func1() {
    console.log(scope);
}

function func2() {
    var scope = 'local';

    func1();
}

func1(); //global
func2(); //global

func1は外部変数「scope」の値を出力します。定義時のscopeの値は’global’ですので、13行目でのfunc1の実行結果は「global」になります。

次にこの関数func1をfunc2の内部で実行しています。このタイミングではfunc1の外部変数scopeの値は’local’になっています。 にもかかわらず、ここで出力される値は’global’となります。

このことは関数はいかなるタイミングや場所で実行されようとも、定義時のコンテキストで実行されるということを表しています。


次に、JavaScriptの関数が持つ、もう一つの重要な特性を挙げます。

2.JavaScriptの関数は第1級オブジェクトである。

第1級オブジェクトとは、通常のオブジェクトの様に、変数に格納したり、他の関数の引数として渡すことができるものという意味です。これはJavaScriptの関数は定義されたコンテキストとは異なるコンテキスト上で実行することができるということです。

例えば次のコードのような使い方をすることができます。

var funcA = function () {
        console.log('funcA Called!!');
    },

    funcB = function (fn) {
        fn();
    };

funcB(funcA); //funcA Called!!

上記コードで、関数「funcA」はグローバルスコープ上に定義されていますが、実行時のスコープは、funcBのローカルスコープ内です。


ここまでの説明を踏まえると、「関数はどこにでも持ち出せるが、あくまでも定義時のコンテキストで実行される」ということが言えます。

そして、関数がクロージャとして独特の振る舞いをするのは定義時のコンテキストとは異なるコンテキスト上で実行される場合のみです。これが最後のポイントです。

3.関数は定義時のコンテキストとは異なるコンテキスト上に持ち出されるとクロージャになる

このことを、クロージャの説明でよく使われるカウンターを例にして説明します。

var createCounter = function () {
        var cnt = 0;

        return function () {
            cnt += 1;
            console.log(cnt);
        };
    };


var counter = createCounter();
counter(); // 「1」が出力される
counter(); // 「2」が出力される
counter(); // 「3」が出力される
・
・
・

コード冒頭のcreateCounterという関数に注目してください。
この関数は、その内部で新たに関数を定義して、その関数を戻り値として返します。JavaScriptの関数は第1級オブジェクトですので、関数の戻り値として使うこともできます。またこの例では無名関数を定義して返していますが、通常の名前付き関数でも構いません。

そして、戻り値として返されるこの関数からみた「定義時のコンテキスト」にはcreateCounter関数のローカル変数(ここでは「cnt」)、およびグローバル変数が含まれています。

コードの11行目で、変数counterには、createCounter関数により作成された関数が格納されます。 この関数を実行すると、1,2,3・・・という具合に数値が出力されます。この値はcreateCounter関数内のローカル変数cntがインクリメントされているものです。

普通に考えたら、コードの11行目でcreateCounter関数の実行が完了しているので、その内部変数であるcntは破棄されてしまう、と考えてしまいそうですが、コードの12行目以降を見ても明らかにcntは保持され続けています。

これは、11行目で変数counterに代入された関数が実行時ではなく、定義時のコンテキストで実行されているためです。

上記コードの11行目の変数「counter」には、createCounterにより作成された関数が格納されますが、この関数の定義時のコンテキスト(createCounter関数のローカルスコープ)と変数counterのコンテキスト(グローバルスコープ)が異なるため、counterに格納される関数はクロージャになります。

クロージャは、関数と、その関数の定義時のコンテキストをセットにした特殊なオブジェクトですので、createCounter関数の実行が完了しても、その内部変数cntはクロージャ内に保持されているため参照し続けることが可能なのです。


少し余談ですが、たまに「クロージャは関数を返す関数」といった説明も見かけますが、それは誤りです。
上記で説明した通り、あくまでクロージャとは「関数とその関数の定義時のコンテキストのセット」にすぎず、「関数を返す」というのはクロージャを扱う過程の話だからです。

・・・といったことを書きつつも、個人的にはクロージャという言葉の定義の意味を知ること自体にはあまり意味がないと考えています。重要なのは、上記で説明したような関数の特性をしっかりと理解しておくことでしょう。そうすれば必然的にクロージャは扱えているはずだからです。


もしかしたらクロージャ、なんていう言葉は無かった方がみんな幸せだったかもしれませんね。

なぜクロージャを使うのか?

クロージャを使うことは、あくまでもプログラミング上のテクニックのようなものです。ですので、JavaScriptにおいて、クロージャを使わなければ実現出来ない機能というのはあり得ません。
ではなぜクロージャは使われるのでしょうか?以下にクロージャが良く使われるケースを解説します。

グローバル変数を使わずに関数に「状態」を持たせる(疑似プライベート変数)

先ほどのカウンターの例で説明すると、インクリメントされる変数(ここではcnt)をグローバル変数にしてしまえば同様の機能を実現できます。

var cnt = 0;

function countUp() {
    cnt += 1;
    console.log(cnt);
}

countUp(); // 「1」が出力される
countUp(); // 「2」が出力される
countUp(); // 「3」が出力される
・
・
・

しかし、上記のようにむやみにグローバル変数を定義してしまうのはJavaScriptの世界ではアンチパターンとされます。複雑なWEBアプリケーションのようなものを作ろうとした場合、あっという間にグローバル変数で埋め尽くされてしまい、コード量が増えるにつれ変数名の競合が起こりやすくなってしまうからです。
また、このやり方だとカウンターを複数作りたい場合に、カウンターの数だけグローバル変数を増やさなければならない、といった点にも問題があります。

グローバル変数を使用せずに同様の機能を実現させるための方法の1つとしては、変数cntをオブジェクトのプロパティにしてしまうことです。

var counter = {
    cnt: 0,
    countUp: function () {
        this.cnt += 1;
        console.log(this.cnt);
    }
};

counter.coutUp(); // 「1」が出力される
counter.coutUp(); // 「2」が出力される
counter.coutUp(); // 「3」が出力される

上記のコード例では、counterというオブジェクトの中のプロパティとしてcntを定義しています。このように、オブジェクトのプロパティを使うことでグローバル変数の使用を抑えることができます。

しかし、この方法だとcounterオブジェクトの外部から、cntプロパティの値を書き換えることが出来てしまいます。通常、このようなケースにおいてはcntプロパティをプライベート変数にするのが望ましいのですが、JavaScriptにはプライベート変数を定義するための構文は用意されていません。つまり、オブジェクトのプロパティは全てパブリックになります。

このような場合、クロージャを使うことでcntを疑似的にプライベート変数にすることができます。

var counter = (function () {
    var cnt = 0;

    return function () {
        cnt += 1;
        console.log(cnt);
    };
}());

counter(); // 「1」が出力される
counter(); // 「2」が出力される
counter(); // 「3」が出力される
上記コードは冒頭のカウンターの例を書き換えたもので、createCounterという関数を定義する代わりに、即時関数を使用しています。 この場合、コードの1行目で即時関数内部で定義された関数が変数couterに格納されますが、この内部で定義された関数からみた「定義時のコンテキスト」には変数cntが含まれていますので、変数counterが破棄されるまでcntは保持され続けます。

このように、クロージャを活用することによってグローバル変数やオブジェクトのプロパティを使用しなくても関数に「状態」を持たせることができます。

またこの場合、変数cntに外部からアクセスすることは出来ないため、一種のプライベート変数のように扱うことができます。JavaScriptには、言語としてプライベート変数を定義するような構文は有りませんが、クロージャを活用することで、疑似的にプライベート変数を実現することができます。

クロージャをうまく使うことで、グローバル変数を減らし、よりスマートで、かつメンテナンス性の高いコードを書くことができます。

関数を作る関数

上記のカウンターの例では、計算の内容が「1を加算する」という固定的なものでした。では、この計算の内容を例えば「1を減算する」「2を加算する」といったものに置き換えるにはどうすれば良いでしょうか?

先ほどのカウンターのコードを少し修正して関数の実行時に引数を渡せるようにしたものが下記のコードです。

var counter = (function () {
    var cnt = 0;
 
    return function (num) {
        cnt += num;
        console.log(cnt);
    };
}());
 
counter(-1); // 「-1」が出力される
counter(-1); // 「-2」が出力される
counter(-1); // 「-3」が出力される

このようなやり方でも期待する結果を得ることができますが、下記のように関数の定義時に変更したい値を引数として渡すことでも実現可能です。

var makeCounter = function (num) {
        var startNum = 0;

        return function () {
            console.log(startNum += num);
        };
    },

    incrementer = makeCounter(1),
    decrementer = makeCounter(-1);

incrementer(); //1
incrementer(); //2
incrementer(); //3

decrementer(); //-1
decrementer(); //-2
decrementer(); //-3

このように、JavaScriptのクロージャは「関数を作る関数」として活用することもできます。

まとめ

JavaScriptはとにかく関数が重要な言語です。クロージャも例外ではありません。関数を理解できればクロージャを本質から理解することができるでしょう。

それでは、今回の解説のポイントを順にまとめていきます。

クロージャとは

クロージャとは、関数と、その関数の定義時のコンテキストをセットにした特殊なオブジェクトです。

クロージャを理解するポイント

  1. 関数は定義時のコンテキストで実行される(※重要)
  2. 関数は第一級オブジェクトなので、定義時のコンテキストとは異なるコンテキスト上で実行することができる
  3. 関数は定義時のコンテキストとは異なるコンテキスト上にある変数に代入された時にクロージャになる

クロージャの用途

一般的には、クロージャは次の様な目的で利用されることが多いと考えられます。

  • グローバル変数を使わずに関数に「状態」を持たせる(疑似プライベート変数)
  • 関数を作る関数

クロージャの利用はあくまでもプログラミング上のテクニックであって必須ではありません。そのため、クロージャでなければ実現できない機能というのはあり得ません。

ですが、クロージャをうまく活用することで無駄の無い、メンテナンス性の高いコードを書くことができるでしょう。

▼参考にさせて頂いたサイト