Kodokon kodokon.com

クロージャとレキシカルスコープ

クロージャが変数をどのように捕捉するかを習得し、状態をカプセル化してスコープの落とし穴を避けられるようになりましょう。

9 分 · 3 問

このレッスンを Kodokon で開く

クロージャは何か特殊な裏技ではありません。それは関数が、呼び出された場所ではなく 定義された レキシカルスコープへのアクセスを保ち続ける仕組みです。エンジンは値をコピーするのではなく、レキシカル環境への 生きた参照 を保持します。したがって同じファクトリを2回呼び出すと、互いに独立した2つの環境が生成され、これがクロージャをこの言語で最も軽量なカプセル化ツールにしています。

JAVASCRIPT
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    reset: () => { count = 0; },
  };
}
const counterA = createCounter();
const counterB = createCounter();
counterA.increment();
counterA.increment();
console.log(counterA.increment()); // 3
console.log(counterB.increment()); // 1
プライベートな状態: count は返されたメソッドを通してのみ到達できます。

実務でよくある古典的な罠: var はブロックではなく、それを囲む関数に束縛されます。ループの内側では、各クロージャは 同じ 変数を捕捉することになります。let を使うと、各反復ごとに新しいレキシカル環境が作られるため、各捕捉が別々になります。さらに 一時的デッドゾーン(TDZ)もあります。letconst の変数はブロックに入った瞬間から存在しますが、宣言より前に読み取ろうとすると ReferenceError を投げます。

JAVASCRIPT
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0); // 0, 1, 2
}
var は単一の束縛を共有し、let は反復ごとに1つ作ります。

パターンの観点では、クロージャは メモ化 とモジュールパターンの基礎です。クラスと比べると、クロージャは this やプロトタイプなしで本物のプライバシーを提供します。その代わり、数千のインスタンスを作る場合はメモリ使用量がやや多くなります。各インスタンスが自前の関数を持つためです。

JAVASCRIPT
function memoize(fn) {
  const cache = new Map();
  return function (key) {
    if (cache.has(key)) return cache.get(key);
    const result = fn(key);
    cache.set(key, result);
    return result;
  };
}
const square = (n) => n * n;
const fastSquare = memoize(square);
console.log(fastSquare(9)); // computed
console.log(fastSquare(9)); // read from the cache
キャッシュはクロージャの内部に存在し、外からは見えません。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. for (var i = ...) ループの中で、ループ本体の内側で作られた3つの setTimeout コールバックは何をログ出力しますか?
    • 0, 1, 2。各コールバックが自分自身の値を捕捉するから
    • 3, 3, 3。すべてが i の同じ束縛を共有するから
    • undefined を3回。i はループの後で破棄されるから
  2. クロージャは、それが定義された環境から正確には何を保持しますか?
    • 生成時点の値の凍結されたコピー
    • 捕捉した変数への生きた参照
    • それを囲む関数の引数だけ
    • 何も保持しない。関数が返ると同時にスコープは破棄される
  3. なぜ createCountercount がプライベートに保たれることを保証するのですか?
    • let が変数を不変にするから
    • count はそのスコープを捕捉する唯一の存在である、返された関数を通してのみ到達できるから
    • ファクトリが返った後、エンジンが count を削除するから