memolu

いろいろメモってます

クロージャを解説してみる

ここではクロージャについて解説します。
javascriptを触り始めると、プロトタイプとクロージャあたりがつまづきやすいかと思います。
仕組みがわかっても使いどころがわからないという意見も聞きますので、少し踏み込んだところまで学習してみましょう。

クロージャの背景

クロージャを概念として説明すると少しわかりづらくなってしまうため、先に背景から説明します。
プログラムは実行された際に、変数の値などをマシンのメモリ上に保存します。そのままメモリを使い続けるといつかクラッシュしてしまうため、プログラムが実行されて必要ないと判断された値はメモリ上から削除される仕組みがあります。
C言語などではこのメモリ解放をプログラマが実装するようにコードを記述する必要があります。
Javajavascriptでは必要がない値はなどは自動的に削除を行っており、メモリ上の余分なデータは常に解放され続ける仕組みをとっています。これをガベージコレクションと呼びます。
クロージャの説明にはガベージコレクションの理解が密接しているため、これを先に解説します。

ガベージコレクション

(function() {
    var name = 'yt-ymmt';
})();

上のコードでいうと即時実行関数で変数nameを宣言しyt-ymmtを代入していますが、他のどこでも使用されていないため、関数実行後に変数nameはメモリ上から削除されます。
ガベージコレクションはこのメモリ解放の働きを指します。

var user = (function() {
    var name = 'yt=ymmt';

    return {
        name: name
    };
})();

console.log(user.name); // yt-ymmt

それでは上のコードではどうでしょうか。
変数userに即時実行関数でオブジェクトが返るようにしました。オブジェクトのnameプロパティには関数内で宣言した変数nameをそのまま代入しています。
実はこの場合でも変数nameは関数実行後にアクセスする必要がなくなるため、ガベージコレクションされてしまいます。user.nameに代入されているのは文字列yt-ymmtでしかなく、変数nameの値ではないのです。
それでは変数nameをガベージコレクションさせない方法はどうしたらよいでしょう。

var user = (function() {
    var name = 'yt-ymmt';

    return {
        name : function(){
               return name;
        }
    }
})();

console.log(user.name); // yt-ymmt

上のコードでは変数userに返すオブジェクトのnameプロパティに関数を登録しました。そして呼び出されるために変数nameの値を取得しにいくように処理されています。
このコードパターンで変数nameがガベージコレクションされてしまうと、変数nameがundefinedを返すようになってしまいます。そのため、これであれば変数nameはガベージコレクションされないのです。

クロージャとは?

ここまで理解できればクロージャの概念を把握することは簡単です。
クロージャとは変数に作成したコンテキスト外からアクセスできるようにすることでガベージコレクションされない仕組みを指します。
もう少し例を書いてみます。

var user = function(name) {
    return function() {
        return name;
    }
};

var yt = user('yt');
var ymmt = user('ymmt');

console.log(yt); // yt
console.log(ymmt); .. ymmt

上のコードでは、userに関数を代入しています。
user関数は引数としてnameを受け取ります。関数内では無名関数をそのまま返し、無名関数内では引数として渡されたnameをそのまま返すように処理されています。
この仕組みは上で説明したように毎回引数として渡された変数nameにアクセスするようになるため、変数nameはガベージコレクションされません。

クロージャの使いどころ

クロージャは理解できましたが、実際の使用例がわかりづらいのが特徴です。
クロージャの利用シーンはコードによって様々ですが、例として以下の項目が挙げられるかと思います。

  • グローバル名前空間の汚染を防ぐ
  • 関数を作るための関数

グローバルの汚染を防ぐ

クロージャを使用すると宣言された変数は関数の中にカプセル化することができます。
するとカプセル化した変数は他の関数などからアクセスされることはなくなりますので、名前空間の汚染を最小限にすることができるでしょう。
下のコードはよく挙げられるカウンター関数です。アンチパターンと一緒に説明します。

// アンチパターン
var count = 0;
 
function countUp() {
    count += 1;
    console.log(count);
}
 
countUp(); // 1
countUp(); // 2
countUp(); // 3

↑の場合、変数countが別の関数などでグローバル宣言されると上書きされてしまいます。

// クロージャパターン
var createCounter = function () {
        var count = 0;
 
        return function () {
            count += 1;
            console.log(count);
        };
    };
 
var counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3

↑の場合、変数countが関数スコープで守られているため、他の関数などからはアクセスできません。
このコードであれば、むやみにグローバルの名前空間を汚染せず、命名によるバッティングリスクを抑えることができます。

関数を作るための関数

個人的にはこれがクロージャの本質に近いのかなと感じています。
上で記載したコードは機能がcountをインクリメントするだけでした。ではインクリメントだけではなくデクリメントしたいとき、もしくは+2を行いたい際どうしたらいいのでしょうか。
しかしこれもクロージャを使用することで関数を2つ作成する必要がなくなります。

var createCounter = function(num) {
    var counter = 0;

    return function() {
        return counter += num;
    }
};

var incre = createCounter(2);
var decre = createCounter(-1);

console.log(incre()); // 2
console.log(incre()); // 4
console.log(incre()); // 6

console.log(decre()); // -1
console.log(decre()); // -2
console.log(decre()); // -3

このコードパターンであれば関数を2つ作成する必要もなく機能を切り分けることができました。
関数を書かずに容量の圧縮や処理の簡潔さにも繋がっており、非常に優れたコード設計になります。

まとめ

このようにクロージャはちょっとわかりづらいですが、理解できればjavascriptの言語仕様をうまく使った強力なコードデザインパターンを利用することができるようになります。
クロージャをどこでも使用してしまうと、メモリリークとなりパフォーマンス面でもボトルネックになってしまうため、使い所をしっかり検討した上で実装することが望ましいとされています。
クロージャは必須の機能ではなく、このコードパターンを使用しなくとも同じ機能を実装することは可能です。しかし、理解できているといないのとでは、他人のコードを読む際の理解度に差異が出てきたり、自分のコードの保守性など様々な面でメリットがあると考えられます。
ぜひこの機会に理解を深め、実践できるようになりましょう!