JavaScriptのクロージャーについて

2021年2月8日JavaScript

JavaScriptのクロージャーについてまとめました。

クロージャー(closures)について

クロージャーとは、レキシカルスコープの変数を関数が使用している状態のことです。

クロージャーを使ったプライベート変数の定義

プライベート変数というのは、関数の外部からアクセスできない変数のことです。

function incFactory() {
  let num = 0;
  function inc() {
    num += 10;
    console.log(num);
  }
  return inc;//関数を返す
}

const inc = incFactory();//incには関数が返されているので実行できる
inc();//10
inc();//20
inc();//30

このnumという変数は、外のスコープからはアクセスできない点に注目です。
変更される心配がなくなります。
また、incFactory()が実行されたときにのみnumは初期化されます。

クロージャーを使った動的な関数の生成

クロージャー状況に応じて変化する関数を作成することができます。

function addNumFactory(num) {
  function addPrice(price) {
    return num * price;
  }
  return addPrice;
}

const add5 = addNumFactory(5);
const add10 = addNumFactory(10);
const result1 = add5(10);
const result2 = add10(10);
console.log(result1);//50
console.log(result2);//100

このように、numに与える値によって返す値が変化する関数を作ることができます。

クロージャーを具体的にみてみる

まず、下のような関数の中に関数の例をみてみましょう。

//closures

const secureBooking = function() {
  let passengerCount = 0;

  return function() {
    passengerCount++;
    console.log(`${passengerCount} 名`);
  }
}

const booker = secureBooking();
booker();//1 名
booker();//2 名
booker();//3 名

console.dir(booker);
//dirのscopeが変数環境でクロージャーの中身を確認できます。

コールスタックの状態

この場合、コールスタックに積み上げられるのは、グローバル実行コンテキストのsecureBooking関数のみとなります。
そしてsecureBooking関数が呼び出されて実行されると中の返される予定の関数がコールスタックに積み上げられます。

スコープチェーンの状態

スコープチェーンはどうなっているでしょうか?passengerCountという変数はsecureBooking内ではアクセス可能となりますが、グローバルスコープからではアクセスできない状態になっています。
ここで関数スコープ内にある返される関数が実行されたとき、その関数はコールスタッフから取り除かれます。

ここが重要なポイントとなります。もうコールスタックにはグローバル実行コンテキストしかない状態となります。

booker()はグローバル実行コンテキストにある関数です。booker()関数実行コンテキスト内は、何も変数が宣言されていないため、空っぽの状態です。
またbooker()関数のスコープチェーンは、グローバル実行コンテキストのみとなります。

ここで疑問が生じます。

passengerCount変数はグローバル実行コンテキストにもbooker()関数内にもないのにどうやってアクセスできるのでしょうか?

booker()関数が作られた変数環境の実行コンテキストはsecureBooking()関数です。

そして、関数は、作られた環境自体にアクセスできます。そのため、secureBooking()関数の実行コンテキスト内で作られたはずのpassengerCount = 0にアクセス可能となります。

このコネクションのことを、クロージャー(closures)といいます。

クロージャーのポイント

重要な部分を整理します。

  • 関数は、その関数が作られた実行コンテキストの変数環境へアクセス可能です。
  • それは、その実行コンテキストがコールスタックから取り除かれた後でもアクセス可能です。
  • クロージャーは、正確にその関数が作られたときと場所の関数がある変数環境です。

bookerの実際の挙動

const booker = passengerCount = 0という意味になります。

内部関数が実行されると、const booker = passengerCount = 1となり、これがスコープチェーンよりも優先されます。

そのため次は、const booker = passengerCount = 2となり、カウントがどんどん増えていくことができます。

 

クロージャーの具体例:変数環境で値が変わる

他のクロージャーの具体例もみて、クロージャーの動きをより深く理解しておきましょう。

//1st example

let first;

const second = function() {
  const third = 20;
  first = function() {
    console.log(third * 3);
  }
}

second();
first();//60

secondはもうないはずなのにthirdにアクセスできています。クロージャーの典型的な例になります。
これにさらに関数を追加したパターンも見ておきましょう。

 

let first;

const second = function() {
  const third = 20;
  first = function() {
    console.log(third * 3);
  }
}

const forth = function () {
  const fifth = 100;
  first = function () {
    console.log(fifth * 2);
  }
}

second();
first();//60が返される。

//first関数が再度割り当てられる
forth();
first();//200が返される。
console.dir(first);

上の例では、first()はsecondの中のthirdの値を参照していたので60が返ってきていましたが、今回は、呼ぶ順番を変更すると、200が返ってきています。
これは、forthの中のfifthの値を参照しています。

クロージャーの具体例:スコープチェーンよりも優先される

const announce = function(n, wait) {
  const perGroup = n / 4;

  setTimeout(function(){
    console.log(`只今、全${n}名のお客様がご乗車されております`);
    console.log(`4グループに分かれて、それぞれ${perGroup}名となります`);
  }, wait * 1000);

  console.log(`乗車までの待ち時間は${wait}分です`);
};

const perGroup = 300;
announce(200, 3);

上の例では、announce関数が実行されたあと、しばらくしてからその内部関数であるsetTimeoutの関数が実行されているのがわかります。
その中にはannounce関数の引数やその中で宣言されている変数もしっかりと活用できています。これもクロージャーがあることの例です。
また、perGroupについて、const perGroup = 300が無視されていることで、スコープチェーンよりもクロージャーが優先されていることがわかります。

クロージャーまとめ

  • クロージャーは、関数が作られた実行コンテキストの閉ざされた変数環境のことです。それは、その実行コンテキストがコールスタックからなくなった後でも存在しています。
  • 言い換えると、クロージャーは、関数に親の関数のすべての変数へのアクセスを与えています。それは、その親の関数がreturnされた後もアクセスを与え続けます。
  • その関数は、アウタースコープがなくなった後もアウタースコープへの参照を保持し続け、それが一貫してスコープチェーンを保持することになります。
  • さらに、言い換えると、クロージャーは、その関数が自身の生まれた場所の中で存在する変数へのコネクションを失わないようにしてくれています。たとえ、その生まれた場所が存在しなくなってもです。
  • クロージャーは、関数がどこにでも持ち運べるかばんのようなもので、そのかばんの中には関数が作られたときの環境に存在した変数が詰まっています。
  • console.dirを使うと、scopesが変数環境を意味しています。そのscopesの中でクロージャーの項目で中身を確認することができます。