クロージャとは?スコープを覚えている関数
クロージャとは、定義されたときに捕捉した周囲の変数と対になった関数であり、そのスコープが終わった後でもそれらの変数を使い続けられます。
ある関数が別の関数の中で定義されると、その関数は外側の関数の変数を参照できます。クロージャとは、その内側の関数と、捕捉した変数への生きたリンクを合わせたものです。外側の関数が戻った後でも、クロージャはそれらを生かし続け、読み書きできるため、クロージャは強力であり、時に驚かせるものになります。
クロージャが捕捉するもの
クロージャは、生成されたスコープの変数への参照を捕捉するのであって、その瞬間の値だけを捕捉するのではありません。したがって、捕捉した変数が後で変わると、クロージャは新しい値を見ます。これがクロージャにプライベートな状態を維持させる仕組みです。
クロージャが役立つ理由
- カウンターやcacheのような、関数の背後に隠されたプライベートな状態。
- 特化した関数を生み出すファクトリ関数。
- 生成された場所のコンテキストを覚えているcallback。
- 一部の引数を事前に固定する部分適用。
古典的なループの落とし穴
よくあるバグは、ループ内で同じループ変数をすべて捕捉するクロージャを生成することです。言語または新しいバインディングが変数を反復ごとにスコープしない限り、各クロージャは自分の反復時の値ではなく最終的な値を見ます。
クロージャとメモリ
クロージャは捕捉した変数を生かし続けるため、意図せず大きなオブジェクトを保持し、ガベージコレクションを妨げ得ます。長寿命のリストやレジストリに保存されたクロージャは、よくある静かなメモリリークの原因です。
手短な例
ローカルの count をインクリメントする関数を返す makeCounter() はクロージャを生成します。返される各関数は、呼び出し間で持続する自分専用のプライベートな count を持ちます。
CIにおけるクロージャ
大きな捕捉オブジェクトを保持するクロージャは、長いテスト実行の間にメモリを増やし、小さなrunnerでOOM killを引き起こし得ます。より大きなrunner(Latchkey)はより多くの余裕を与え、一時的なOOM killを自動でリトライしますが、捕捉しすぎるクロージャは依然として修正に値します。
重要なポイント
- クロージャは関数に、それが定義された場所の捕捉された変数を加えたものである。
- 参照を捕捉するため、後の変更も見え、プライベートな状態を保持できる。
- クロージャは大きなオブジェクトを保持し得て、長期保存するとメモリをリークし得る。