JS クロージャの基礎
クロージャ (Closures) は、外部関数が実行を終了した後でも、その周囲のスコープ (Surrounding Scope) にある変数に関数がアクセスすることを可能にします。クロージャを理解することで、強力なプログラミングパターンを解き放ち、一般的な落とし穴を回避できるようになります。
本章では、クロージャとは何か、どのように機能するのか、そしてなぜそれほどまでに有用なのかを詳しく探っていきます。
1. クロージャを理解する:基礎知識
本質的に、クロージャは「関数」とその関数が宣言された「レキシカル環境 (Lexical Environment)」の組み合わせです。
この環境には、関数が作成された時点のスコープ内にあったすべての変数が含まれます。関数はこれらの変数を「閉じ込める (Closes over)」ため、元のスコープの外で実行されたとしても、それらの変数へのアクセス権を保持し続けます。
これを説明するために、シンプルな例を見てみましょう:
function outerFunction(outerVar) {
function innerFunction(innerVar) {
console.log(outerVar, innerVar);
}
return innerFunction;
}
const myInnerFunction = outerFunction("こんにちは");
myInnerFunction("世界"); // 出力: こんにちは 世界この例では、innerFunction がクロージャです。outerFunction が値を返して終了した後でも、innerFunction は outerFunction のスコープから outerVar にアクセスできます。myInnerFunction を呼び出したとき、それは依然として outerVar の値を保持し、使用することができるのです。
2. レキシカル環境 とスコープ
クロージャをマスターするには、レキシカルスコープ (Lexical Scope) の理解が不可欠です。
レキシカルスコープとは、関数がソースコード内の「どこに配置されているか」によって、その関数のスコープが決定されることを意味します。上の例では、innerFunction はレキシカル(静的)に outerFunction の内部にネストされているため、outerFunction の変数にアクセスできます。
これは実行時のコールスタックによって決定される動的スコープ(JavaScript は動的スコープを採用していません)とは異なります。
3. クロージャが状態 (State) を保持する仕組み
クロージャは、関数がその閉じ込められたスコープ内の値を「記憶」することを可能にします。この状態を保持する (Preserve state) 能力は、クロージャの最も強力な機能の一つです。
別の例を見てみましょう:
function createCounter() {
let count = 0;
function increment() {
count++;
console.log(count);
}
return increment;
}
const counter1 = createCounter();
counter1(); // 出力: 1
counter1(); // 出力: 2
const counter2 = createCounter();
counter2(); // 出力: 1
counter1(); // 出力: 3このケースでは、increment がクロージャであり、count 変数にアクセスしています。
createCounter を呼び出すたびに、独自のプライベートな count 変数を持つ新しいクロージャが作成されます。これにより、複数の独立したカウンタを作成することが可能になります。この count 変数は increment 関数を通してのみアクセスできるため、実質的にプライベートな変数となります。
4. 多重にネストされたクロージャ
クロージャはネストさせることができます。関数は、自分を囲んでいるすべての周囲のスコープから変数にアクセス可能です。
function grandParentFunction(grandParentVar) {
function parentFunction(parentVar) {
function childFunction(childVar) {
console.log(grandParentVar, parentVar, childVar);
}
return childFunction;
}
return parentFunction;
}
const parent = grandParentFunction("祖父 (GrandParent)");
const child = parent("父親 (Parent)");
child("子供 (Child)"); // 出力: 祖父 (GrandParent) 父親 (Parent) 子供 (Child)ここでは、childFunction は grandParentFunction と parentFunction の両方から変数にアクセスできています。
5. クロージャの実践的な例
クロージャの理解を深めるために、より実戦に近い例を見ていきましょう。
5.1 カプセル化 (Encapsulation)
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit: function(amount) {
balance += amount;
return balance;
},
withdraw: function(amount) {
if (balance >= amount) {
balance -= amount;
return balance;
} else {
return "残高不足";
}
},
getBalance: function() {
return balance;
}
};
}
const myAccount = createBankAccount(100);
console.log(myAccount.deposit(50)); // 出力: 150
console.log(myAccount.withdraw(20)); // 出力: 130
console.log(myAccount.getBalance()); // 出力: 130
// console.log(myAccount.balance); // undefined. balanceはクロージャによってプライベート化されていますこの例では、balance 変数は createBankAccount によって作成されたクロージャに対してプライベートです。外部のコードは balance に直接アクセスしたり修正したりすることはできず、deposit(預金)、withdraw(引き出し)、getBalance(残高確認)といったメソッドを介してのみ対話できます。これはカプセル化の一形態であり、オブジェクトの内部状態を保護するのに役立ちます。
5.2 モジュール (Modules) の作成
クロージャは、JavaScript においてモジュールを作成するためにも使用されます。モジュールは、データと振る舞いを含む独立したコード単位です。
const myModule = (function() {
let privateVariable = "秘密 (Secret)";
function privateMethod() {
console.log("privateMethod 内部:", privateVariable);
}
return {
publicMethod: function() {
console.log("publicMethod 内部");
privateMethod();
}
};
})();
myModule.publicMethod();
// 出力: publicMethod 内部
// privateMethod 内部: 秘密 (Secret)
// myModule.privateMethod(); // エラー - 関数ではありません
// console.log(myModule.privateVariable); // undefinedこの例では、IIFE(即時実行関数式)がクロージャを作成し、privateVariable と privateMethod をカプセル化しています。これらはモジュール外部からはアクセスできません。モジュールは、プライベートメンバにアクセスできる publicMethod のみを公開しています。
5.3 イベントハンドラ (Event Handlers)
クロージャは、イベントハンドラが作成された時点で使用可能だったデータにアクセスするために、イベントハンドラでよく使用されます。
<!DOCTYPE html>
<html>
<head>
<title>クロージャの例</title>
</head>
<body>
<button id="myButton">ここをクリック</button>
<script>
function setupButton(buttonId, message) {
const button = document.getElementById(buttonId);
button.addEventListener("click", function() {
alert(message);
});
}
setupButton("myButton", "ボタンがクリックされました!");
</script>
</body>
</html>この例で addEventListener に渡されている匿名関数はクロージャです。この関数は setupButton 関数の message 変数にアクセスできます。setupButton の実行が完了した後でも、イベントハンドラは依然として message の値を保持し、使用することができます。