クロージャ、カプセル化と秘匿化

クロージャとオブジェクト

クロージャに初めて触れたとき、全く理解できなかった記憶があります。クロージャを正しく理解するには、JavaScriptのコア知識が少なからず必要です。簡単かつ迅速に理解するには、MDNのクロージャパートの上段に書かれている内容が適切なようです。

Closures are functions that refer to independent (free) variables (variables that are used locally, but defined in an enclosing scope). In other words, these functions ‘remember’ the environment in which they were created.

つまり、生成された当時の環境を記憶する関数です。環境はスコープチェーン自体を指しますが、スコープチェーンを通じてアクセスできる変数や関数が、スコープが解除されるべき時点でも消えないということです。このようなスコープは、オブジェクトが持つ性質であるカプセル化と秘匿化を実装するのに使用できます。クロージャが生成され、状態を含む行為を結びつけられるようになりますが、このようにクロージャがオブジェクトを生成するもう1つの方法であると考えることもできます(もちろん、オブジェクトをどのように定義するかによって異なりますが)。コンテキストをthisでアクセスするオブジェクトとは違い、スコープでアクセスします。

この記事では、クロージャの活用方法について、いくつか紹介したいと思います。

カウンターの実装

カウンターの実装は、クロージャを説明する例としてよく登場します。

一般オブジェクトを利用したカウンター

一般オブジェクトを用いてカウンターを構成すると、次のようになります。

var counter = {
    _count: 0,
     count: function() {
        return this._count += 1;
    }
}

console.log(counter.count()); // 1
console.log(counter.count()); // 2

オブジェクトリテラルでオブジェクトを作成し、オブジェクト内に_countというアトリビュートを使って数字を1つずつカウントしています。
カウンターを複数作成できるように、コンストラクタに実装してみよう。

function Counter(){
    this._count = 0;
}

Counter.prototype.count = function() {
    return this._count += 1;
};

var counter = new Counter();
var counter2 = new Counter();

console.log(counter.count()) //1
console.log(counter.count()) //2
console.log(counter2.count()) //1

thisを使ってコンテキストにアクセスする一般オブジェクトです。
ここで重要なのは、_countという変数を使った点です。その変数の値を増加させる行為を持つ関数が存在するということです。

  • 数字を保存する_countというメンバ変数
  • 値を増加させる行為をするメンバ関数count()

クロージャを利用したカウンター

上記の内容をクロージャを用いて実装してみよう。

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

    return function() {
        return _count += 1;
    };
})();

console.log(counter());
console.log(counter());

コードは若干異なるかもしれませんが、生成されたオブジェクトを見ると、コンテキストの_countという変数にthisを用いてアクセスしていたものを、今回はスコープを通じてアクセスしただけで、大差なく同じ動作をします。構成要素も同様に数を格納する_countという変数があり、countをする関数があります。オブジェクトのカプセル化と秘匿化に符合します。
カウンターを複数作成できるように生成関数を作ってみよう。IIFEを記名関数に変更するだけです。関数にはファクトリという名前を付けました。

function counterFactory() {
    var _count = 0;

    return function() {
        _count += 1;

      return _count;
   };
}

var counter = counterFactory();
var counter2 = counterFactory();

console.log(counter()); //1
console.log(counter()); //2
console.log(counter2()); //1

生成される関数の個数のパフォーマンスに疑問がありますが、ここでは性能ではなく、実装内容に集中しましょう。
その時その時の状況に合わせて活用できることが、より重要です。長い関数は分けて位置を移し、重複を除去する方法もあります。

このようにクロージャを使って実装すると、コンテキストにアクセスする際にスコープを使ってアクセスするため、thisというキーワードを使う必要もありません。こうして作られたカウンターはどのオブジェクトに付けて使用しても、同じコンテキストの結果を出し、イベントリスナーで用いても同じコンテキストを維持した状態で使用できます。

var counter = counterFactory();

var app = {
    counter: counter
};

var app2 = {
    counter: counter
};

console.log(app.counter()); //1
console.log(app.counter()); //2

console.log(app2.counter()); //3

秘匿化の面から見ると、むしろオブジェクトではなく、クロージャの活用でさらに極大化されます。_countという変数は、外部からアプローチする方法が全くないためです。
JavaScriptのオブジェクトは、プロパティの外部アクセスがすべて許容されます。クロージャ以外は遮断する方法がありません。

カリー化を利用したカウンター

今までのカウンターは、数字を1つずつ増加させるように作りましたが、これからはインクリメント値が異なるファクトリを作ってみよう。作成関数は、厳密に言えばファクトリのファクトリです。そこでファクトリメーカーに定めました。ここでは簡単にカリー化を使って作成しますが、JavaScriptでのカリー化はクロージャで簡単に実装できます。

function counterFactoryMaker(incValue) {
    return function factory(initValue) {
        var _count = initValue;

        return function counter() {
            return _count += incValue;
        };
    };
}

var counterFactory = counterFactoryMaker(2);
var counter = counterFactory(0);
var counter2 = counterFactory(1);

console.log(counter()); // 2
console.log(counter()); // 4

console.log(counter2()); // 3
console.log(counter2()); // 5

そもそも引数が1つなのでカリー化が成立しない、という意見を避けるため、初期値もつけられるようにしました。それぞれの関数を理解するために記名関数を使って関数を返却しました。factory()の立場では、counterFactoryMaker()と呼ばれる1つのスコープを加えたクロージャであり、counter()の立場では、counterFactoryMaker()とfactory()のスコープを持ったクロージャです。JavaScriptの開発では、カリー化を積極的に応用して開発していませんが、lodashのような関数型プログラミングライブラリで用いるHaskell技法、Lazy evalutionのような場合は、カリー化、すなわちパーシャルアプリケーションの利点を最大限に活かして、大規模なパフォーマンスの向上が期待できます。最近、一般化されたパイプラインを提供する関数型プログラミングライブラリであるRamdaも登場しています。

オブジェクトとクロージャを混用したカウンター

JavaScriptで何かを開発するときは、オブジェクトとクロージャを適切に混用しながら開発することになります。クロージャを使わずにイベントリスナーを適用すると問題が生じます。一方でクロージャですべてのものを作ると、それもまた問題点があります。そこで適切に混用して使用します。オブジェクトリテラルでオブジェクトを生成し、クロージャを組み合わせると、JavaScriptには存在しないアクセス制限を真似ることができます。今回は動作に対応するメソッドを少し追加しました。

function counterFactory2() {
    var _count = 0;

    function count(value) {
        _count = value || _count;

        return _count;
    }

    return {
        count: count,
        inc: function() {
            return count(count() + 1);
        },
        dec: function() {
            return count(count() - 1);
        }
    };
}

var counter = counterFactory2();

console.log(counter.inc());
console.log(counter.inc());
console.log(counter.dec());

JavaScriptのシングルトンを実装する際に頻繁に使用されるModule Patternの典型的な形です。非表示の内容は、クロージャ内部に隠し、インターフェースだけ外部に露出させます。inc()メソッドとdec()メソッドは、それぞれの値を増加、減少させる動作をします。アクセス制限は使用していませんが、ここでは_countというメンバがクロージャに隠れているprivateメンバであると言えます。先に説明したようにクロージャだけに存在する変数は、外部からはどのような方法でもアクセスできません。しかし、ダグラス・クロックフォードが言った”privileged method”を別途作成し、間接的にprivateメンバにアクセスする方法も用意できます。count()関数がprivileged methodの役割をするのです。count()関数に引数を渡すと_count変数に値がセットされ、引数を渡したり、また渡さなくても_countの値を返却してくれます。つまり値を超えた場合、セッターで値を渡さなければゲッターとして動作します。Objective-Cでは、propertyにメンバを宣言すると、自動で作成されるアクセスメソッドと同様の役割をします。このようにprivileged methodを実装すると、オーバーライドで、外部からオブジェクトを拡張することも可能です。
counterFactory2が作るカウンターを拡張してみよう。

function counterFactory2Ext() {
    var counter = counterFactory2();
    var count = counter.count;

    counter.inc = function() {
        return count(count() + 2);
    };

    return counter;
}

var counterExt = counterFactory2Ext();

console.log(counterExt.inc()); // 2
console.log(counterExt.inc()); // 4
console.log(counterExt.inc()); // 6
console.log(counterExt.dec()); // 5

counterFactory2()を通じて作られたオブジェクトのメソッドを上書きしてオーバーライドしました。カウンターはinc()関数で2ずつ値を増加させることになります。上記の実装内容は、データを持っているクロージャと、行為を持っているオブジェクトの組合せです。この方法はあまり使用されてはいませんが、状況によっては良い方法になることもあります。上記の内容を、コンストラクタを使って再作成すると、以下のような実装も可能です。

function Counter() {
    var _count = 0;

    this.count = function(value) {
        _count = value || _count;
        return _count;
    }
}

Counter.prototype.inc = function() {
    var count = this.count;

    return count(count() + 1);
};

Counter.prototype.dec = function() {
    var count = this.count;

    return count(count() - 1);
};

var counter = new Counter();

console.log(counter.inc()); // 1
console.log(counter.inc()); // 2
console.log(counter.inc()); // 3

コンストラクタをクロージャスコープに活用しました。このようなタイプの実装は、隠したいメンバがプリミティブ型ではなく、オブジェクトのとき、セッターを置かずにゲッターのみクロージャを利用する方式の方が、もう少し実用的でしょう。

まとめ

クロージャを置いてオブジェクトと整理すると当然無理を伴いますが、カプセル化と秘匿化を実装するもう1つの方法としてクロージャを理解すると、より簡単にアクセスできます。クロージャとオブジェクトをうまく組み合わせて、JavaScriptならではの長所を活かしたオブジェクト指向プログラミングの実装に役立てられればと思います。

参考

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop