jQueryとJavaScriptにおけるクロージャの実践的理解

クロージャの基礎概念

クロージャの定義

クロージャはスコープという概念を核としています。端的に言うと、ある関数の内部に定義された別の関数から、その外側の関数の変数にアクセスできる 기능을 말합니다。

実際の開発では、データの隠蔽化やコールバック関数の処理で頻繁に利用されます。

// 即時実行関数(典型的なクロージャのパターン)
(function(globalObj){  
    globalObj.alert("アプリケーション起動");
})(window);

// パラメータ付き即時実行
(function(signal){ 
    globalObj.console.log(signal);  
})(999);

(function(jq){  
    jq("div p").click(function(){
        globalObj.alert("要素がクリックされました");
    });  
})(jQuery);

上記のコードでは、関数のパラメータとして$を受け取り、内部でjQueryオブジェクトとして動作させています。この形式はIIFE(Immediately Invoked Function Expression)と呼ばれ、スコープを隔离しながらコードを安全に実行できます。

クロージャの実例

基本的なパターン

function createCounter()
{
    var count = "カウンター変数値";

     return function()
     {
         globalObj.alert(count);
      }
}

var getCounter = createCounter();
getCounter();

このコードの実行結果は「カウンター変数値」と表示されます。

通常の変数ライフサイクル而言えば、createCounter関数の実行が終了するとcount変数はメモリから解放されるべきです。しかし、実際には解放されません。これは無名関数がcount変数を「キャプチャ」しており、変数のライフサイクル延长させているからです。これがクロージャの本質です。

関数実行の詳細

(function(framework){
     framework("div p").click(処理関数);
})(jQuery);    

// 上記は以下の形式で書くこともできる
function initializeHandler(framework){  
     framework("div p").click(処理関数);
}
initializeHandler(jQuery);
(function(callback){
     callback("div p").click(処理関数);
})(jQuery);

基本的な即時実行構文:

// 匿名関数を定義して即座に実行
(function(){処理内容})();

// パラメータ付きバージョン
(function(引数){処理内容})(実引数);

注意すべき点として、即時実行関数内で宣言した変数はローカルスコープを持ちます:

(function(){var localVar = "ローカル値"})();
globalObj.alert(localVar);  //  ReferenceError: localVar is not defined

クロージャの長所と短所

長所:グローバル変数を増やさずに済む。実行中のすべての変数は匿名関数内で闭环されるため、セキュリティ상有利です。

短所:適切に管理されない場合、メモリリークを引き起こす可能性があります。

オブジェクト指向からのクロージャ理解

Javaのクラス定義を例に挙げると:

public class DataManager
{
    private string _internalData;
 
    public void processData()
    {
        // 処理実装
    }
}

JavaScriptにおいても类似した構造を作れます:

function outerScope()
{
    var privateVar = 0;
    function innerHandler(){
    }
}

この場合、outerScopeを「外部関数」、innerHandlerを「内部関数」と呼びます。

外部関数はクラスのように、内部変数はプライベートフィールドのように、内部関数はメソッドのように動作します。Javaでクラスのインスタンスを生成してメソッドを呼び出すように、JavaScriptでは外部関数の外側から内部関数を呼び出すことができ、これを「クロージャ」と呼びます。

内部関数の呼び出し方法

方法1:外部関数の外側から直接呼び出し(失敗例)

function outerScope() {
    globalObj.console.log('外部関数実行中');
    function innerHandler() {
        globalObj.console.log('内部関数実行中');
    }
}

(function() {
    innerHandler();  // エラー発生
})();

結果:Uncaught ReferenceError: innerHandler is not defined

内部関数のスコープは外部関数内に限定されているため、外部からはアクセスできません。

方法2:外部関数内で内部関数を呼び出し(成功例)

function outerScope() {
    globalObj.console.log('外部関数実行中');
    function innerHandler() {
        globalObj.console.log('内部関数実行中');
    }
    innerHandler();
}

(function() {
    outerScope();
})();

結果:外部関数実行中 内部関数実行中

方法3:グローバル変数に代入

var globalHandler;

function outerScope() {
    globalObj.console.log('外部関数実行中');
    function innerHandler() {
        globalObj.console.log('内部関数実行中');
    }
    globalHandler = innerHandler;
}

(function() {
    outerScope();
    globalObj.console.log('グローバル変数経由で呼び出し:');
    globalHandler();
})();

結果:外部関数実行中 グローバル変数経由で呼び出し: 内部関数実行中

方法4:戻り値として返す

function outerScope() {
    globalObj.console.log('外部関数実行中');
    function innerHandler() {
        globalObj.console.log('内部関数実行中');
    }
    return innerHandler;
}

(function() {
    var temporary = outerScope();
    globalObj.console.log('変数経由で実行');
    temporary();
})();

結果:外部関数実行中 変数経由で実行 内部関数実行中

変数のスコープ

内部関数のローカル変数

function outerScope() {
    function innerHandler() {
        var temp = 0;
        temp++;
        globalObj.console.log('内部変数temp: ' + temp);
    }
    return innerHandler;
}

(function() {
    var first = outerScope();
    first();
    first();

    var second = outerScope();
    second();
    second();
})();

結果:内部変数temp: 1 内部変数temp: 1 内部変数temp: 1 内部変数temp: 1

この结果から、内部関数の変数は関数と同じ生命周期を持つことがわかります。每次呼び出すと、新しい変数が作成されます。

グローバル変数

var temp = 0;
function outerScope() {
    function innerHandler() {
        temp++;
        globalObj.console.log('グローバル変数temp: ' + temp);
    }
    return innerHandler;
}

(function() {
    var first = outerScope();
    first();
    first();

    var second = outerScope();
    second();
    second();
})();

結果:グローバル変数temp: 1 グローバル変数temp: 2 グローバル変数temp: 3 グローバル変数temp: 4

外部関数の変数

function outerScope() {
    var temp = 0;
    function innerHandler() {
        temp++;
        globalObj.console.log('外部関数の変数temp: ' + temp);
    }
    return innerHandler;
}

(function() {
    var first = outerScope();
    first();
    first();

    var second = outerScope();
    second();
    second();
})();

結果:外部関数の変数temp: 1 外部関数の変数temp: 2 外部関数の変数temp: 1 外部関数の変数temp: 2

この结果から、外部関数の変数は外部関数と同じ生命周期を持ちます。新的外部関数を生成するたびに、新的変数環境が作られます。

複数の内部関数が変数を共有する場合

function outerScope() {
    var temp = 0;
    function incrementHandler() {
        temp++;
        globalObj.console.log('incrementHandler実行後: ' + temp);
    }
    
    function doubleHandler() {
        temp += 2;
        globalObj.console.log('doubleHandler実行後: ' + temp);
    }

    return {inc: incrementHandler, dbl: doubleHandler};
}

(function() {
    var instance = outerScope();
    instance.inc();
    instance.dbl();
    instance.inc();

    var another = outerScope();
    another.inc();
    another.dbl();
    another.inc();
})();

結果:incrementHandler実行後: 1 doubleHandler実行後: 3 incrementHandler実行後: 4 incrementHandler実行後: 1 doubleHandler実行後: 3 incrementHandler実行後: 4

jQueryにおけるクロージャ

DOM準備完了時の処理

(function($){
    $(document).ready(function() {
        var counter = 0;

        function updateDisplay() {
            counter++;
            globalObj.console.log(counter);
        }

        updateDisplay();
        updateDisplay();
    });
})(jQuery);

結果:1 2

$(document).ready()のパラメータは匿名外部関数として動作し、その中の関数が内部関数になります。

イベントハンドラでの応用

(function($){
    $(document).ready(function() {
        var clickCount = 0;

        $('#incrementBtn').click(function(event) {
            clickCount++;
            globalObj.console.log(clickCount);
        });

        $('#decrementBtn').click(function(event) {
            clickCount--;
            globalObj.console.log(clickCount);
        });
    });
})(jQuery);

2つのボタンのクリックイベント(内部関数)が同一个外部関数変数clickCountを共有しています。

ループでの典型的な落とし穴

(function($){
    $(document).ready(function() {
        for (var index = 0; index < 3; index++) {
            $('<div>Item ' + index + '</div>').click(function() {
                globalObj.console.log(index);
            }).appendTo('#container');
        }
    });
})(jQuery);

画面表示:Item 0 Item 1 Item 2

しかし、各divをクリック했을console.logには常に「3」が表示されます。これはindexがループ終了後に3になっているためです。すべてのクリックハンドラが同一个変数indexを参照しているためです。

解決策:$.each()を使用

(function($){
    $(document).ready(function() {
        $.each([0, 1, 2], function(key, value) {
            $('<div>Item ' + value + '</div>').click(function() {
                globalObj.console.log(value);
            }).appendTo('#container');
        });
    });
})(jQuery);

この方法では、每次の反復処理ごとに異なるvalue値がクロージャにキャプチャされるため、期待通りの動作になります。

メモリリークの原因と対策

メモリリークとは、アプリケーション中的変数が对应的メモリを確保한狀態で、適切に解放되지ない状態を指します。

シンプルな参照関係

オブジェクトAがBを参照し、BがCを参照する場合、Aが不要になればBとCも自動的にガベージコレクションの対象になります。

循環参照の問題

AがBを参照し、BがCを参照し、さらにCがBを参照する場合、Aが解放されてもBとCは自動的に解放されません。手動での処理が必要です。

内部関数によるメモリリーク

(function($){
    var dataObject = {};

    function processFunction() {
        globalObj.console.log(dataObject);
    }

    dataObject.handler = processFunction;
    return processFunction;
})();

dataObjectはメモリ上のオブジェクトであり、processFunction同样是メモリ上のオブジェクトです。dataObject.handlerがprocessFunctionを指し、processFunctionがdataObjectを参照するため、循環参照が発生します。手動で處理しないとメモリリークになります。

内部関数が外部変数を明示的に参照하지ない場合でも、関数が呼び出される際に外部関数の変数を暗黙的に参照するため、同样にメモリリークのリスクがあります。

イベントハンドラによるメモリリーク

古いブラウザ(IE)では以下の記述会导致内存泄漏:

(function($){
    $(document).ready(function(){
        var btnElement = document.getElementById("myButton");
        btnElement.onclick = function(){
            globalObj.console.log('クリック');
            return false;
        };
    });
})();

以下の記述は安全です:

function handleClick(){
    globalObj.console.log('クリック');
    return false;
}

(function($){
    $(document).ready(function(){
        var btnElement = document.getElementById('myButton');
        btnElement.onclick = handleClick;
    });
})();

jQueryを使用する場合、この点は自動的に処理されるため、心配不要です:

(function($){
    $(document).ready(function(){
        var $button = $('#myButton');
        $button.click(function(event){
            event.preventDefault();
            globalObj.console.log('クリック');
        });
    });
})();

まとめ

クロージャを理解する上で重要なのは、変数のスコープ、JavaScriptのガベージコレクション机制、メモリリークへの影響这三点です。実践を通じて理解を深めていくことが 바랍니다。

タグ: jQuery javascript クロージャ スコープ メモリ管理

8月6日 19:35 投稿