JavaScriptのクロージャとスコープを簡単に解説
別の関数を返すJavaScript関数を書いているとします。内側の関数は外側の関数の変数を使用していますが、後で呼び出すと、外側の関数はすでに終了しています。それでも変数はまだそこにあります。なぜでしょう?これがクロージャとスコープの魔法です。多くの開発者がこの概念で苦労し、予期しない変数値やメモリリークなどのバグにつながります。この記事では、JavaScriptのクロージャとスコープを実用的な例を交えて簡単に説明します。
JavaScriptにおけるスコープとは?
スコープは、コード内で変数がどこからアクセスできるかを決定します。JavaScriptには主に3つのスコープがあります:
- グローバルスコープ:関数やブロックの外で宣言された変数はグローバルにアクセス可能です。
- 関数スコープ:関数内で宣言された変数はその関数内でのみアクセス可能です。
- ブロックスコープ:
letとconstでブロック{}内で宣言された変数はそのブロック内でのみアクセス可能です。
簡単な例を示します:
let globalVar = 'I am global';
function myFunc() {
let functionVar = 'I am in function scope';
if (true) {
let blockVar = 'I am in block scope';
console.log(globalVar); // accessible
console.log(functionVar); // accessible
console.log(blockVar); // accessible
}
console.log(blockVar); // ReferenceError: blockVar is not defined
}
myFunc();
console.log(functionVar); // ReferenceError: functionVar is not defined
スコープを理解することは、変数の衝突を防ぎ、メモリ管理に役立つため重要です。
レキシカルスコープ:クロージャの基盤
JavaScriptはレキシカルスコープを使用します。つまり、変数のスコープはソースコード内の位置によって決まります。内側の関数は外側(囲む)スコープの変数にアクセスできます。これは静的スコープとも呼ばれます。
次の例を考えてみましょう:
function outer() {
let outerVar = 'outer';
function inner() {
console.log(outerVar); // 'outer'
}
inner();
}
outer();
inner関数はouterVarにアクセスできます。なぜなら、それはレキシカルにouterの内側にあるからです。これは動的ではなく、関数の呼び出し方に依存しません。
クロージャとは?
クロージャとは、関数がそのレキシカルスコープの外で実行されても、そのスコープを記憶している関数です。言い換えれば、クロージャは内側の関数から外側の関数のスコープへのアクセスを提供します。クロージャは関数が作成されるたびに、関数作成時に作成されます。
例:
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
ここで、makeCounterはcountをインクリメントする内側の関数を返します。makeCounterの実行が終了した後でも、返された関数はcountにアクセスできます。これがクロージャです。
クロージャの内部動作
関数が作成されると、そのレキシカル環境(スコープチェーン)への参照を保存します。関数が呼び出されると、その環境を使用して変数を解決します。環境はクロージャが存在する限り生き続けます。
クロージャの実用的な用途
クロージャは理論だけでなく、JavaScriptのいたるところで使用されています。
1. データプライバシー(モジュールパターン)
ES6モジュール以前は、クロージャを使用してプライベート変数を作成していました:
const counterModule = (function() {
let count = 0;
return {
increment: function() { count++; },
getCount: function() { return count; }
};
})();
counterModule.increment();
console.log(counterModule.getCount()); // 1
console.log(counterModule.count); // undefined
ここで、countはプライベートであり、返されたメソッドを通じてのみアクセスできます。
2. イベントハンドラとコールバック
クロージャは、外側のスコープから変数にアクセスするためにイベントハンドラで一般的に使用されます:
function setupButton() {
let clickCount = 0;
document.getElementById('myButton').addEventListener('click', function() {
clickCount++;
console.log(`Button clicked ${clickCount} times`);
});
}
イベントハンドラのクロージャはclickCountを記憶します。
3. 関数ファクトリ
プリセット引数を持つ関数を作成できます:
function multiplyBy(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplyBy(2);
console.log(double(5)); // 10
よくある落とし穴とその回避方法
クロージャは理解していないと予期しない動作を引き起こす可能性があります。
ループ変数のキャプチャ
ループでvarを使用すると、単一の共有バインディングが作成されます:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 3, 3, 3
}, 100);
}
すべてのコールバックが同じiを共有します。修正するには、let(ブロックスコープ)またはIIFEを使用します:
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 0, 1, 2
}, 100);
}
メモリリーク
クロージャは変数を生き続けさせます。クロージャが長命のオブジェクト(DOM要素など)にアタッチされている場合、ガベージコレクションを妨げる可能性があります。不要になったイベントリスナーは常に削除してください。
スコープチェーンと変数解決
変数にアクセスすると、JavaScriptはスコープチェーンを検索します:まず現在のスコープ、次に外側のスコープ、グローバルスコープまで。見つからない場合はReferenceErrorがスローされます。
例:
let a = 'global';
function outer() {
let b = 'outer';
function inner() {
let c = 'inner';
console.log(a, b, c); // all accessible
}
inner();
}
outer();
innerのスコープチェーンは:inner -> outer -> globalです。
比較:var、let、const
| キーワード | スコープ | ホイスティング | 再代入可能 |
|---|---|---|---|
| var | 関数 | はい(undefinedで初期化) | はい |
| let | ブロック | はい(ただし初期化されず、TDZ) | はい |
| const | ブロック | はい(ただし初期化されず、TDZ) | いいえ |
これらの違いを理解することが、スコープ関連のバグを避ける鍵です。
FAQ
JavaScriptのクロージャとは何ですか?
クロージャとは、そのレキシカルスコープの外で実行されても、そのスコープへのアクセスを保持する関数です。関数が定義された場所の変数を「記憶」することができます。
なぜクロージャが重要ですか?
クロージャは、データプライバシー、関数ファクトリ、状態を維持するイベントハンドラを可能にします。多くのJavaScriptパターンの基礎です。
クロージャ関連のバグを避けるにはどうすればよいですか?
ブロックスコープにはletとconstを使用し、ループに注意し、メモリリークを防ぐためにイベントリスナーをクリーンアップしてください。
理解度をテストする準備はできましたか?JSON Formatterを試して、コードスニペットを実験し、クロージャがリアルタイムでどのように機能するかを確認してください。