クロージャを理解する

関数は作られた場所の変数へ後からもアクセスできます。クロージャを、値の保存という曖昧な言い方にとどめず、どの変数の束縛を参照するのかコードで追います。

この単元で学ぶこと

  • 作成場所のスコープと呼出し場所を区別する
  • 外側関数の終了後に残る状態を追う
  • 別呼出しと共有変数の違いを説明する

本文解説

クロージャとは 関数と、その関数が作られたときの外側の字句環境の組合せをクロージャと呼びます。関数内に見つからない変数は、定義された場所の外側へ順に探します。関数を呼んだ場所に同じ名前の変数があっても、通常それを代わりに読む仕組みではありません。 【前提】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ループの関数作成と後日の実行を混同しない。

用語解説

字句環境
コード中の宣言と外側環境のつながりを管理する実行上の仕組み。
束縛
識別子と、その値や格納先との対応。
クロージャ
関数と参照する外側の字句環境の組合せ。

関連教材

参考資料

教材データに登録された参考リンクです。リンク先の内容や更新日は提供元でご確認ください。

確認問題の出題・採点や学習履歴の保存にはログインが必要です。