🚴

JavaScriptのクロージャとは?

2021/01/12に公開

まずはMDNによるクロージャーの定義はこちら。

クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせで>す。言い換えれば、クロージャは内側の関数から外側の関数スコープへのアクセスを提供します。JavaScript >では、関数が作成されるたびにクロージャが作成されます。
参照: https://developer.mozilla.org/ja/docs/Web/JavaScript/Closures

これを読んでも理解できなかったので、詳しく調べてみました。

クロージャーを理解する上で、まずダイナミックスコープとレキシカルスコープの違いを理解する事が大事そうだったので説明します。

ダイナミックスコープとレキシカルスコープ

JavaScriptは親の関数の変数を子の関数内で使える。
下のコードでいうと、親の関数が(ichiro), 子の関数が(jiro)であり、parentが親の関数の変数であり、この変数を子の関数のjiro内で使っている。

const ichiro_function = () => {
  const parent = "ichiro"; // 親の関数の変数

  const jiro_function = () => {
    const jiro = "jiro"; //子の関数の変数
    console.log(jiro);
    console.log(parent); //子の関数内で親の関数の変数を使用
  };
  return jiro_function();
};

ichiro_function();

//実行結果
//jiro
//ichiro

反対にJavaScriptは子の関数の変数を親の関数内で使うとnot definedになります。

const ichiro_function = () => {
  const parent = "ichiro"; // 親の関数の変数

  const jiro_function = () => {
    const jiro = "jiro"; //子の関数の変数
  };
  
  console.log(jiro) //親の関数内で子の関数の変数を使用
  
  return jiro_function();
};

ichiro_function();

//実行結果
// jiro is not defined

それを踏まえた上で、次はichiro_functionと兄弟関係にあるsaburo_functionという関数を追加し、
その関数内にichiro_functionと同じ変数名であるparentを定義します。
更にsaburo_functionという関数は引数にichiro_functionを渡し、定義します。

const ichiro_function = () => {
  const parent = "ichiro"; // 親の関数の変数

  const jiro_function = () => {
    const jiro = "jiro"; //子の関数の変数
    console.log(jiro);
    console.log(parent); //子の関数内で親の関数の変数を使用
  };
  return jiro_function;
};

// ichiro_functionと兄弟関係のsaburo_function
const saburo_function = (f) => { // ichiro_functionを引数に渡す
  const parent = "sabro";        // ichiro_functionと同じ変数名
  f();                // ichiro_functionを定義
};

const ichiro = ichiro_function();

saburo_function(ichiro);

この時に大事な疑問点があります。
saburo_function内に引数として渡された「f」はichiro_functionであり、実行された時には、console.log(parent)が実行されます。(子の関数であるjiro_function関数内で定義されている)
しかし、ichiro_functionにもsaburo_functionにも「parent」という変数が定義されています。
どちらの「parent」の値が実行時に出力されるのでしょうか!?

二郎の親は一郎なのか、三郎なのか…

この答えが、ダイナミックスコープ(動的スコープ)とレキシカルスコープ(静的スコープ)に関係しています。

ダイナミックスコープの考え方では、二郎の親は三郎になり、

レキシカルスコープの考え方では、二郎の親は一郎になります。

まとめ

つまり、実行された時の親の関数が、親の関数であるとする考え方をダイナミックスコープ。

実行される場所は関係なく、定義された時の親を親とする考え方をレキシカルスコープと言います。

関数の親の決め方には、大きく分けてダイナミックスコープ(動的スコープ)とレキシカルスコープ(静的スコープ)があり、JavaScriptはレキシカルスコープを採用しています。

ここでもう一度クロージャの定義に戻ります。

組み合わされた関数と、レキシカル環境への参照の組み合わせ。

ここで僕なりにまとめると

親および祖先の関数内に現れる変数をローカルスコープだけでなく、関数が定義された場所のスコープも含めて参照する関数の事をクロージャと呼びます。
そして、その参照をする際の考え方は大きく分けてダイナミックスコープとレキシカルスコープがあります。

間違った理解をしている点や適切な説明じゃない点などがある場合は、コメントなどを頂けると助かります。

ここまで読んで頂きありがとうございました。

Discussion