クロージャを理解する
関数は作られた場所の変数へ後からもアクセスできます。クロージャを、値の保存という曖昧な言い方にとどめず、どの変数の束縛を参照するのかコードで追います。
この単元で学ぶこと
- 作成場所のスコープと呼出し場所を区別する
- 外側関数の終了後に残る状態を追う
- 別呼出しと共有変数の違いを説明する
本文解説
クロージャとは 関数と、その関数が作られたときの外側の字句環境の組合せをクロージャと呼びます。関数内に見つからない変数は、定義された場所の外側へ順に探します。関数を呼んだ場所に同じ名前の変数があっても、通常それを代わりに読む仕組みではありません。 【前提】JavaScriptコードは各問独立。logはconsole.logと同じ表示関数。示されていない再代入や呼出しはない。標準のJavaScript実行環境を使う。
基本の関数生成 function makeAdder(x){return y=>x+y;} const add3=makeAdder(3); ならadd3(4)は7です。makeAdderの処理が終わっていても、返された関数からその呼出しのxへアクセスできます。xをグローバル変数として作る必要はありません。
値の凍結ではない let n=1; const read=()=>n; n=4; read(); は4です。readは作成時の数値1を固定保存したのではなく、外側のnという束縛を参照しています。後でその束縛の値を変えると読める値も変わります。スナップショットが必要なら別の束縛へ値を保存します。
状態を持つカウンタ function make(){let n=0;return ()=>++n;} const c=make(); とするとc()は1、次は2です。外側の関数が毎回再実行されてn=0になるのではなく、同じcが同じnを更新します。++nは増やしてから値を返す前置増分です。
別の生成は別の状態 const a=make(); const b=make(); と二回作れば、a()を二回呼んで1、2、その後b()は1です。makeの呼出しごとに別のnが作られます。一方const b=aなら関数を新規生成せず同じ関数を参照するので、状態も共有します。
同じ環境を共有する関数 function pair(){let n=0;return {inc:()=>++n,read:()=>n};} の返す二関数は同じ呼出しのnを見ます。const p=pair(); p.inc(); p.read(); は1。読み出しと変更を別関数にしても、環境が共通なら変更を共有できます。
ループと束縛 const fs=[]; for(var i=0;i<3;i++){fs.push(()=>i);} の終了後に呼ぶと全て3です。varのiを共有するためです。for(let i=0;i<3;i++)なら反復ごとの束縛が作られ、0、1、2を返します。どちらも関数を追加した時点では関数本体を実行していません。
用途と保持の注意 クロージャは設定済み関数、イベント処理、状態を外へ直接公開しないAPIに使えます。const api={read:()=>n}でも外のnがletなら更新できます。参照される環境が残るため、不要なイベント登録や大きなデータの保持を見直しますが、クロージャがあるだけで必ずメモリリークとは言えません。
図解
重要ポイント
- 関数は定義された場所の字句環境を参照する。
- 外側変数の値を必ず凍結するわけではない。
- 生成呼出しごとの別状態と同じ環境の共有を区別する。
間違えやすい点
- 呼出し元の同名変数を読むと考えない。
- varループの関数作成と後日の実行を混同しない。
用語解説
- 字句環境
- コード中の宣言と外側環境のつながりを管理する実行上の仕組み。
- 束縛
- 識別子と、その値や格納先との対応。
- クロージャ
- 関数と参照する外側の字句環境の組合せ。
関連教材
参考資料
教材データに登録された参考リンクです。リンク先の内容や更新日は提供元でご確認ください。
確認問題の出題・採点や学習履歴の保存にはログインが必要です。