`Lexical Environment`(レキシカル環境)と`closure`(クロージャ)の関係を理解する
2023-02-19
課題感
closure
の理解があやふやclosure
は、外側の変数の情報を持った関数と言われるが、下記のコードのsayFruit2
の中でsayFruit1
を呼び出した時、🍌でなく🍎が出力されことを感覚では理解していたが、どのような仕様のもとに動作しているのかを知らない
let food = '🍎' const sayFruit1 = () => console.log(food) const sayFruit2 = () => { let food = '🍌' sayFruit1() // disp 🍎. not 🍌 } sayFruit2()
解説
Lexical Environment
という概念の理解が必要- JavaScriptのグローバルスコープ、ブロックスコープ、関数には
Lexical Environment
と呼ばれるオブジェクトを保持している
変数
- 変数は
Lexical Environment
のプロパティとして管理される
let apple = '🍎'
このコードのLexical Environment
は以下のようになる
コードブロック
- コードブロックに処理が移ると新しい
Lexical Environment
が作成される outerEnv
というプロパティに外部(コードブロック前)のLexical Environment
を指し示す- グローバルスコープ
Lexical Environment
のouterEnv
はnull
になる outerEnv
は値の保持ではなく、アドレスを保持(スナップショットではない)
let apple = '🍎' { let banan = '🍌' }
このコードのLexical Environment
は以下のようになる
関数
- 関数オブジェクトが
Lexical Environment
のプロパティに定義される - 関数が呼び出されると関数に対応した新しい
Lexical Environment
が生成される outerEnv
には、関数呼び出し元の参照がセットされる
let apple = '🍎' const sayFruit = (fruit) => { console.log(fruit) console.log(apple) } sayFruit('🍌');
このコードのLexical Environment
は以下のようになる
- 変数にアクセスした場合、最初に自身の
Lexical Environment
を探し、見つからなければouterEnv
を辿っていく outerEnv
がnull
になるまで辿り、見つからなければReferenceError
が発生するsayFruit
関数内でapple
にアクセスする時、自身のLexical Environment
にはapple
がないので、outerEnv
を辿り、グローバルのLexical Environment
でapple
が見つかる
関数を返す関数
- 関数オブジェクトは
[[environment]]
プロパティを持っている [[environment]]
プロパティは、その関数オブジェクトが作られた場所のLexical Environment
を指し示す。※重要
const sayFruitFactory = () => { let sayCount = 0 return (fruit) => { sayCount++ console.log(fruit + sayCount) } } const sayFruit = sayFruitFactory() sayFruit('🍇')
このコードのLexical Environment
は以下のようになる
sayFruit
関数はsayFruitFactory
関数の中で作られた関数なので、[[environment]]
プロパティにはsayFruitFactory
関数のLexical Environment
がセットされているsayFruit
関数が呼び出されると、その外部Lexical Environment
の参照は[[environment]]
から取得される
冒頭のコードの解説
- これまでの説明を踏まえて、冒頭のコードの動作を解説する
sayFruit1
関数の[[environment]]
プロパティにはグローバルのLexical Environment
がセットされている- そのため、
sayFruit1
関数内でfood
にアクセスすると、sayFruit1
関数のLexical Environment
にはfood
がないので、outerEnv
->[[environment]]
と辿り、グローバルのLexical Environment
でfood
を見つけることになる sayFruit1
関数の直前に定義したfood
の🍌
にはアクセスしない
let food = '🍎' const sayFruit1 = () => console.log(food) const sayFruit2 = () => { let food = '🍌' sayFruit1() // disp 🍎. not 🍌 } sayFruit2()
Lexical Environment
は以下のようになる
まとめ
- 変数は
Lexical Environment
のプロパティとして管理される - 関数呼び出しの度に
Lexical Environment
が作られる outerEnv
には、関数呼び出し元の参照がセットされる- 自身の
Lexical Environment
に変数がなければ、outerEnv
を辿っていく - 関数オブジェクトは
[[environment]]
プロパティを持っている [[environment]]
プロパティは、その関数オブジェクトが作られた場所のLexical Environment
を指し示す- これはEcmaScriptの仕様書に書いてあることでブラウザベンダーがどのように実装しているかは別の話とのこと