読み込み中...
読み込み中...
読み込み中...
読み込み中...
読み込み中...
JavaScriptのスコープはレキシカルスコープ(静的スコープ)と呼ばれ、変数のスコープはコードが書かれた場所で決まります。関数が呼び出された場所ではなく、定義された場所の環境を参照する点が重要です。
スコープには大きく3つのレベルがあります。
| スコープ | 説明 | 宣言方法 |
|---|---|---|
| グローバルスコープ | どこからでもアクセス可能 | トップレベルで宣言 |
| 関数スコープ | 関数内でのみ有効 | 関数内で var / let / const |
| ブロックスコープ | {} 内でのみ有効 | let / const(var は対象外) |
変数を参照するとき、JavaScriptエンジンはまず現在のスコープを探し、見つからなければ外側のスコープを順にたどります。この仕組みをスコープチェーンと呼びます。最終的にグローバルスコープまで到達しても見つからなければ ReferenceError が発生します。
スコープチェーンによる変数探索の流れ
const globalVar = "グローバル";
function outer() {
const outerVar = "外側";
function inner() {
const innerVar = "内側";
// inner から全てのスコープにアクセス可能
console.log(innerVar); // "内側"
console.log(outerVar); // "外側"(スコープチェーンで探索)
console.log(globalVar); // "グローバル"(さらに外側)
}
inner();
// console.log(innerVar); // ReferenceError: innerVar is not defined
}
outer();チャレンジ
関数 createGreeting の中で変数 prefix を "Hello" と宣言し、内部関数 greet が prefix と引数 name を組み合わせて "Hello, Alice" のような文字列を返すようにしてください。createGreeting は greet 関数を返してください。
ポイント
JavaScriptはレキシカルスコープを採用しており、変数のスコープはコードの記述位置で決まります。スコープチェーンにより、内側のスコープから外側のスコープへと変数を探索します。let / const はブロックスコープ、var は関数スコープを持つ点に注意しましょう。
前提知識
この章の内容を理解するには、JavaScript基礎コースの変数・データ型・型変換で学ぶ let/const/var のスコープの違いと、関数で学ぶ関数宣言・関数式・アロー関数の基本が前提となります。
**クロージャ(Closure)**とは、関数が自身の外側のスコープにある変数への参照を保持し続ける仕組みです。外側の関数が実行を終えた後でも、内側の関数はその変数にアクセスできます。
これはJavaScriptのレキシカルスコープの性質から自然に生まれる動作です。関数は定義時の環境(レキシカル環境)への参照を内部的に保持しており、関数が返された後も環境オブジェクトがガベージコレクションされずにメモリに残ります。
クロージャの代表的な用途は次の通りです。
| パターン | 説明 |
|---|---|
| プライベート変数 | 外部から直接アクセスできない変数を作る |
| ファクトリー関数 | 設定値を保持した関数を生成する |
| 状態の保持 | カウンターやキャッシュなど、状態を関数内に閉じ込める |
クロージャによるプライベート変数の実装
// プライベート変数パターン
function createCounter() {
let count = 0; // 外部から直接アクセス不可
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
// console.log(count); // ReferenceError: count is not definedクロージャによるファクトリー関数
// ファクトリー関数パターン
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// 各関数が独自の factor を保持しているチャレンジ
関数 createAccumulator を実装してください。この関数は初期値 initial を受け取り、呼び出すたびに引数の値を累積加算する関数を返します。
ポイント
クロージャは関数が外側のスコープの変数への参照を保持する仕組みです。プライベート変数の実現やファクトリー関数、状態管理など、JavaScriptの多くのパターンの基盤になっています。
**IIFE(Immediately Invoked Function Expression、即時実行関数式)**は、定義と同時に実行される関数式です。(function() { ... })() の形で書きます。
ES Modules が普及する前のJavaScriptでは、グローバルスコープの汚染が大きな問題でした。すべてのスクリプトが同じグローバルスコープを共有するため、変数名が衝突するリスクがありました。IIFEは関数スコープを利用して変数を隔離し、この問題を解決する手法として広く使われていました。
現在でもIIFEはレガシーコードで頻繁に見られ、また特定のユースケース(初期化処理の即時実行、ブロックスコープが使えない環境)で有用です。
IIFEの基本形とバリエーション
// IIFE の基本形
(function() {
const secret = "外からアクセス不可";
console.log(secret); // "外からアクセス不可"
})();
// console.log(secret); // ReferenceError
// アロー関数版 IIFE
(() => {
const temp = "一時変数";
console.log(temp);
})();
// 引数を受け取る IIFE
(function(name) {
console.log("Hello, " + name);
})("World");IIFEとクロージャを組み合わせたRevealing Module パターンは、公開するメソッドだけをオブジェクトとして返し、内部の実装を隠蔽するデザインパターンです。
このパターンには以下の利点があります。
| 利点 | 説明 |
|---|---|
| カプセル化 | 内部変数・関数を外部から隠蔽 |
| 名前空間の分離 | グローバルスコープを汚染しない |
| 明確なインターフェース | 公開APIが return 文で一目瞭然 |
Revealing Module パターンによるカプセル化
// Revealing Module パターン
const Calculator = (function() {
// プライベート変数
let history = [];
// プライベート関数
function addToHistory(operation, result) {
history.push({ operation, result });
}
// 公開メソッド
function add(a, b) {
const result = a + b;
addToHistory(`${a} + ${b}`, result);
return result;
}
function getHistory() {
return [...history]; // コピーを返す
}
// 公開するメソッドだけを返す
return { add, getHistory };
})();
console.log(Calculator.add(3, 5)); // 8
console.log(Calculator.add(10, 20)); // 30
console.log(Calculator.getHistory());
// [{operation: "3 + 5", result: 8}, {operation: "10 + 20", result: 30}]チャレンジ
IIFEを使って Counter モジュールを作成してください。increment()、decrement()、getValue() の3つのメソッドを公開し、カウント値は外部からアクセスできないようにしてください。初期値は 0 です。
ポイント
IIFEは定義と同時に実行される関数式で、スコープの隔離に使われます。Revealing Module パターンはIIFEとクロージャを組み合わせ、公開APIと内部実装を分離するデザインパターンです。ES Modules が主流の現在でも、レガシーコードや特定場面で有用です。
クロージャは強力な仕組みですが、不適切に使うとメモリリークの原因になります。クロージャが外側のスコープの変数を参照し続ける限り、その変数はガベージコレクション(GC)の対象になりません。
**ガベージコレクション(GC)とは、プログラムが使わなくなったメモリを自動的に解放する仕組みです。JavaScriptのGCは到達可能性(Reachability)**を基準にしています。グローバルスコープやスタック上の変数からたどれるオブジェクトは「到達可能」とみなされ、GCの対象になりません。
クロージャが参照を保持することで到達可能な状態が維持され、不要なメモリが解放されない場合があります。これがクロージャによるメモリリークの本質です。
クロージャによるメモリリークとその改善
// メモリリークの例: 不要な参照を保持し続ける
function createHeavyClosure() {
const largeData = new Array(1000000).fill("data");
return function() {
// largeData を実際には使わないが、クロージャが参照を保持
console.log("処理実行");
};
}
// largeData がGCされず、メモリを圧迫する可能性
const heavyFn = createHeavyClosure();
// 改善例: 必要なデータだけを保持する
function createLightClosure() {
const largeData = new Array(1000000).fill("data");
const length = largeData.length; // 必要な値だけ取得
return function() {
console.log("データ件数: " + length);
};
// largeData への参照がなくなり、GC対象になる
}ループ内での var とクロージャは、初心者がつまずきやすいパターンです。var は関数スコープのため、ループ変数が全てのクロージャで共有されてしまいます。
| 問題 | 原因 | 解決策 |
|---|---|---|
| ループ変数の共有 | var が関数スコープ | let を使う or IIFE で囲む |
| 不要な参照保持 | 大きなオブジェクトへの参照 | 必要な値だけ抽出する |
| イベントリスナの蓄積 | removeEventListener 忘れ | クリーンアップ処理を実装する |
ループ内クロージャの問題と解決策
// var を使った場合の問題
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log("var: " + i); // 全て 3 が出力される
}, 100);
}
// let を使った場合(正しい動作)
for (let j = 0; j < 3; j++) {
setTimeout(function() {
console.log("let: " + j); // 0, 1, 2 が出力される
}, 200);
}
// IIFE による解決(var 環境向け)
for (var k = 0; k < 3; k++) {
(function(captured) {
setTimeout(function() {
console.log("IIFE: " + captured); // 0, 1, 2 が出力される
}, 300);
})(k);
}チャレンジ
関数 createFunctions を実装してください。0 から 4 までの数値をそれぞれ返す関数の配列を生成します。let を使ってクロージャの問題を回避してください。
ポイント
クロージャが参照を保持し続けると、不要なオブジェクトがGCされずメモリリークの原因になります。大きなデータの参照は必要な値だけ抽出し、ループ内のクロージャでは let を使って変数の共有問題を回避しましょう。
実務での活用
関数の中から外側の変数を参照し続けるクロージャは、「データを隠蔽しながら操作する仕組み」として広く活用されています。ボタンのクリック回数を数えるカウンターや、一度だけ実行される初期化処理はクロージャの典型的な使いどころです。不要になったクロージャへの参照を残したままにするとメモリを圧迫するため、使い終わった参照を null にする習慣が大切です。
用語