クロージャの基礎概念
クロージャの定義
クロージャはスコープという概念を核としています。端的に言うと、ある関数の内部に定義された別の関数から、その外側の関数の変数にアクセスできる 기능을 말합니다。
実際の開発では、データの隠蔽化やコールバック関数の処理で頻繁に利用されます。
// 即時実行関数(典型的なクロージャのパターン)
(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のガベージコレクション机制、メモリリークへの影響这三点です。実践を通じて理解を深めていくことが 바랍니다。