Writing へ戻る
Software

クロージャ(Closure)

概要

クロージャーとは『関数が定義されたスコープ(レキシカル環境)を保持したまま実行される仕組み』を指します。

通常、関数が終わるとそのスコープ内の変数はガベージコレクションで破棄されます。しかし、内部関数が外側の変数を参照している場合、その変数はGCされずに保持されます。

以下では、makeCounter() が呼ばれたとき、makeCounter の実行コンテキストがメモリ上に作られます。 increment 関数オブジェクトが生成され、この時点で count への参照を閉じ込められます。(クロージャーの形成)

JavaScript Closure Scope DiagramLeft: JS source code with scope-colored line highlights. Right: nested scope diagram showing which variables live where.JavaScriptfunction makeCounter() { let count = 0; function increment() { count += 1; return count; } return increment;}const counter = makeCounter();counter(); // → 1counter(); // → 2counter(); // → 3Scope color legendOuter function scopeInner function (closure)Captured variablePersists in memoryKey point vs Python:No "nonlocal" neededJS closures capture outervariables by reference →read and write freely.Global scopeOuter function scopemakeCounter()let count = 0 ← capturedInner function (closure)increment()count += 1;return count;return increment;readscounter()→ 1counter()→ 2counter()→ 3After makeCounter() returns…count stays alive in memory

解説

それでは、より詳細にクロージャーの動きを見ていきましょう。 ここでは、クロージャの動きを定義と参照に分けて考えます。

まずは手続きPをクロージャを利用して、定義します。定義時、手続きPはコンテキストDに対する参照も保存します。 ここで言うコンテキストとは、特定の状態を持った参照x(変数など)を指し、複数の参照情報を保存することができます。 この時、Pの環境(参照の集合)は、その定義コンテキストによって閉じ込められている(closed over)状態と言えます。

呼び出し時(コンテキスト C)において、P はコンテキスト D からの参照を使用し、処理を行います。

クロージャの定義と呼び出し

目的

クロージャの目的は『関数に状態とスコープを持たせること』です。設計の観点から言うと、以下4つに集約されます。

  1. 状態の保持
  2. カプセル化
  3. 関数のカスタマイズ
  4. 非同期・コールバックでのコンテキスト保持

使い方

クロージャは複数のユースケースに適用できます。

1. プライベート変数(Encapsulation)

オブジェクト指向でいう『Privateフィールド』をクロージャで実現できます。Private変数を外側から直接触れないようにする手法で、状態の安全性や保守性を高めることができます。

function createUser(name) { let _name = name; // private return { getName() { return _name; }, setName(newName) { _name = newName; }, }; } const user = createUser("Alice"); console.log(user.getName()); // Alice user.setName("Bob"); console.log(user.getName()); // Bob console.log(user._name); // undefined(cannot access directly)

2. 関数ファクトリー

設定を閉じ込めた関数を量産できます。createMultiplier(2) で作った関数は永遠に 2 を記憶します。

function createMultiplier(multiplier) { return function (value) { return value * multiplier; }; } const double = createMultiplier(2); const triple = createMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15

3. メモ化

計算が重い関数に『記憶力』を与えます。同じ引数なら計算を省略してキャッシュから返します。useMemo や lodash の _.memoize も内部はこのパターンに該当します。

function memoize(fn) { const cache = {}; return function (...args) { const key = JSON.stringify(args); if (cache[key]) { console.log("cache hit"); return cache[key]; } console.log("compute"); const result = fn(...args); cache[key] = result; return result; }; } function slowAdd(a, b) { return a + b; } const fastAdd = memoize(slowAdd); console.log(fastAdd(1, 2)); // compute → 3 console.log(fastAdd(1, 2)); // cache hit → 3

参考