第17章 クロージャ
クロージャとは?
クロージャは、関数とその関数が作成されたレキシカルスコープの組み合わせを指します。要するに、関数内で定義された変数や外部の変数にアクセスする機能を持つ関数です。
なぜクロージャが必要なのか?
JavaScriptには変数のスコープとして大きく分けて2つのタイプがあります。それが、グローバルスコープとローカルスコープです。関数内で定義された変数は、その関数のローカルスコープに属し、関数の実行が終われば通常はその変数へのアクセスは不可能となります。
しかし、クロージャを使用すると、外部関数のスコープに存在する変数に内部関数からアクセスすることができます。これにより、プライベートな変数を持つことができたり、関数に状態を持たせることが可能になります。
クロージャの実例
function outerFunction() {
let count = 0; // この変数はouterFunctionのローカルスコープにあります
function innerFunction() {
count++; // innerFunctionは外部関数の変数countにアクセスできます
console.log(count);
}
return innerFunction;
}
const increment = outerFunction();
increment(); // 1と表示
increment(); // 2と表示
この例で、outerFunctionはinnerFunctionを返しています。そして、innerFunctionはouterFunctionのスコープにあるcountという変数を参照・操作しています。このように、関数の内部関数が外部関数のローカル変数を参照して操作できる仕組みをクロージャと言います。
クロージャの利点
- データのカプセル化: クロージャを使用することで、外部から直接アクセスできないプライベートな変数を作成できます。これにより、変数の意図しない変更を防ぎ、コードの安全性を高めることができます。
- 状態の保持: クロージャは関数が状態を保持するのに役立ちます。これにより、関数が前回の呼び出し時の情報を「覚えている」ことができます。
- 動的関数生成: 実行時に関数を動的に生成し、それに特定のスコープや状態を持たせることができます。
- コードの短縮と整理: クロージャを用いることで、特定のタスクに特化した関数やユーティリティを簡潔に作成することができます。
クロージャの用途
1.ファクトリー関数: 特定の初期値や設定を持つ関数を動的に生成する際にクロージャが使われます。
function makeMultiplier(factor) {
return function (number) {
return number * factor;
};
}
const double = makeMultiplier(2);
console.log(double(5)); // 10
2.即時実行関数式 (IIFE): 変数のスコープを局所的に限定するために、関数を定義と同時に実行するパターンです。
(function() {
const privateVar = "I'm private!";
// この変数はこの関数のスコープ外からはアクセスできない
})();
3.モジュールパターン: クロージャを使用して、公開メソッドとプライベートメソッド/変数を持つモジュールを作成します。
const counterModule = (function() {
let count = 0; // プライベート変数
return {
increment: function() {
count++;
console.log(count);
},
reset: function() {
count = 0;
console.log(count);
}
};
})();
counterModule.increment(); // 1
counterModule.reset(); // 0
4.イベントハンドラ: DOMのイベントリスナ内で、外部の変数にアクセスするためにクロージャが使われることがよくあります。
function attachEventHandler(buttonId, message) {
var button = document.getElementById(buttonId);
// クロージャを利用して、message変数をキャプチャ
button.addEventListener('click', function() {
alert(message);
});
}
attachEventHandler('myButton', 'ボタンがクリックされました!');
4.タイマーとコールバック: setTimeoutやsetIntervalなどの非同期関数で、外部スコープの変数にアクセスする際にクロージャが役立ちます。
function delayedLog(delay, message) {
// クロージャを利用して、message変数をキャプチャ
setTimeout(function() {
console.log(message);
}, delay);
}
delayedLog(2000, '2秒後に表示されます!');
クロージャの注意点
クロージャは非常に強力な機能であり、多くの場面で便利に使用されますが、誤用するとコードの可読性や性能、さらにはバグの原因となることもあります。以下に、クロージャを使用する際のいくつかの注意点を挙げてみましょう。
メモリリーク
- クロージャはスコープ内の変数への参照を保持し続けるため、これが意図しないメモリの消費につながる可能性があります。
- 特に、DOM要素への参照を保持するクロージャが削除されない場合、そのDOM要素はガベージコレクションの対象とならず、メモリリークの原因となります。
不意の変数の変更
- クロージャは外部スコープの変数を「キャプチャ」します。そのため、その変数の値が変更されると、クロージャ内部の挙動も変わる可能性があります。
- これは、例えばループ内でのクロージャの使用など、予期しない挙動を引き起こす場面が考えられます。
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 期待する出力: 0, 1, 2
// 実際の出力: 3, 3, 3
パフォーマンス
- クロージャは関数スコープと変数をキャプチャするため、一定のオーバーヘッドがあります。
- 大量のクロージャを使用すると、特にモバイルデバイスなどリソースが限られている環境での性能への影響が考えられます。
コードの可読性
- クロージャはコードの複雑さを増加させる可能性があります。適切に使用しないと、コードの流れや変数のスコープが読み手にとって理解しにくくなるかもしれません。
- 明確な目的がない場合や、単純なタスクのためだけのクロージャの使用は避けるべきです。
クロージャの「閉じ込め」
- クロージャが多くの変数をキャプチャする場合、それらの変数が変更されないように注意する必要があります。変数が変更されると、クロージャの挙動が意図しないものになる可能性があります。
練習問題1.
次の要件を満たすカウンタ関数createCounterを作成してください。
- createCounter関数を呼び出すと、新しいカウンタ関数を返します。
- このカウンタ関数を呼び出すたびに、内部のカウントが1増加します。
- カウンタ関数を呼び出すと、その時点でのカウントの値を返します。
// 例:
const counter1 = createCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
const counter2 = createCounter();
console.log(counter2()); // 1
練習問題2.
指定された時間後に指定されたメッセージをコンソールに表示する関数delayedMessageを作成してください。この関数は、2つの引数、delayとmessageを取ります。
- delayはミリ秒単位での遅延時間です。
- messageは遅延時間後にコンソールに表示されるメッセージです。
// 例:
delayedMessage(3000, "3秒後にこれが表示されます");
練習問題3.
オブジェクトを返すcreatePerson関数を作成してください。このオブジェクトは、setNameとgetNameの2つのメソッドを持っていますが、名前を格納する変数はプライベート(外部からアクセスできない)である必要があります。
// 例:
const person = createPerson();
person.setName("Taro");
console.log(person.getName()); // "Taro"
console.log(person.name); // undefined or Error