avatar

Tetsuya Ohira's Blog

Software Developer in Japan 🚀

`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は以下のようになる

variable

コードブロック

  • コードブロックに処理が移ると新しいLexical Environmentが作成される
  • outerEnvというプロパティに外部(コードブロック前)のLexical Environmentを指し示す
  • グローバルスコープLexical EnvironmentのouterEnvはnullになる
  • outerEnvは値の保持ではなく、アドレスを保持(スナップショットではない)
let apple = '🍎' { let banan = '🍌' }

このコードのLexical Environmentは以下のようになる

code block

関数

  • 関数オブジェクトがLexical Environmentのプロパティに定義される
  • 関数が呼び出されると関数に対応した新しいLexical Environmentが生成される
  • outerEnvには、関数呼び出し元の参照がセットされる
let apple = '🍎' const sayFruit = (fruit) => { console.log(fruit) console.log(apple) } sayFruit('🍌');

このコードのLexical Environmentは以下のようになる

function

  • 変数にアクセスした場合、最初に自身の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は以下のようになる

return function

  • 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は以下のようになる

summary

まとめ

  • 変数はLexical Environmentのプロパティとして管理される
  • 関数呼び出しの度にLexical Environmentが作られる
  • outerEnvには、関数呼び出し元の参照がセットされる
  • 自身のLexical Environmentに変数がなければ、outerEnvを辿っていく
  • 関数オブジェクトは[[environment]]プロパティを持っている
  • [[environment]]プロパティは、その関数オブジェクトが作られた場所のLexical Environmentを指し示す
  • これはEcmaScriptの仕様書に書いてあることでブラウザベンダーがどのように実装しているかは別の話とのこと

参考