[EffectiveJavaScript輪読会2]クロージャと仲良くしよう

GAS

どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。

前回のおさらい

前回は、「ローカル宣言は、必ず宣言しよう」をお届けしました。

今回は、「クロージャと仲良くしよう」 をお届けします。

テキスト第2章「変数のスコープ」の項目11に対応しています。※担当回です。気合いが入ります。

今日のアジェンダ

  • メモリの破棄と保持
  • 関数に戻り値を持たせた時のメモリの保持
  • 関数の戻り値に関数をもたせる
  • 関数式によるクロージャ
  • クロージャは、外側の変数を更新できる
  • クロージャとクラス

メモリの破棄と保持

スコープのおさらいですが、const宣言は 関数スコープを生成 しますので、関数を飛び出しません(関数の外から参照できません)。

これは、内部的には、関数を抜けるとメモリを破棄 しています。もう、2度と使わないからです。

メモリを破棄するのは、変数に限った話ではありません。

関数自体も、その後使われることがなければ(記述していてもコメントアウトしていれば使わないこととイコールです。)、メモリは破棄されます。

このようなコードには、メモリを保持するタイミングがありませんので、ブレークポイントも置けません。

function myFunction2_11_01() {

  function makeSandwich() {
    const vegetable = 'レタス';
  }
  // console.log(vegetable); //ReferenceError: vegetable is not defined

  // const sw = makeSandwich(); //コメントアウトは使わないこととイコール
  // console.log(sw);

}

関数を抜けたあと、変数に代入するなど、関数を使うコードを記述すると、メモリは保持されます。

function myFunction2_11_02() {

  function makeSandwich() {
    const vegetable = 'レタス';
  }
  // console.log(vegetable); //ReferenceError: vegetable is not defined

  const sw = makeSandwich();

}

ブレークポイントを置いて、デバッガで確認してみましょう。 

関数に戻り値を持たせた時のメモリの保持

このことを応用して、関数に戻り値 を持たせてみましょう。

実行すると、関数makeSaladsのメモリは破棄されますが、関数makeSandwichは、後で変数swに代入されているので、メモリは破棄されません。

さらに、関数makeSandwichを抜けた時点で変数vegetableのメモリは破棄されますが、戻り値として、変数vegetableをもっていますので、値’レタス’を取得できます。

function myFunction2_11_03() {

  function makeSalads() {
    const vegetable = 'レタス';
  }

  function makeSandwich() {
    const vegetable = 'レタス';
    return vegetable;
  }

  const sw = makeSandwich(); //戻り値vegetableを格納している
  console.log(sw); //レタス

}

関数の戻り値に関数をもたせる

それでは最後に、関数makeSandwichの戻り値に関数make をもたせてみましょう。

おさらいですが、関数makeは「スコープチェーンとして、変数vegetableを参照します」。

関数makeSandwichの戻り値は、関数makeです。ということは、「スコープチェーンとして、変数vegetableを参照します」も、戻り値と一緒に連れてきます 。

function myFunction2_11_04() {

  function makeSandwich() {

    let vegetable = 'レタス';
    function make(topping = 'なし') {
      return vegetable + 'and' + topping;
    }

    return make;
  }

  const sw = makeSandwich(); //戻り値に関数makeを格納している
  console.log(sw); //[Function: make]

  console.log(sw()); //レタスandなし
  console.log(sw('トマト')); //レタスandトマト

}

この、参照先(データを保存してる場所)をもつ関数のことを、クロージャと呼びます。 

クロージャという名前の由来

関数makeは変数vegetableを参照します。変数vegetableは関数make内では定義されていません。

このような変数を 自由変数 と呼びます。

関数makeは自由変数を含んでいるので、開いた関数 です。

そして、関数makeSandwichの内部にある変数名対応表(のようなもの)では、’レタス’とvegetableという 名前が結合 しています。

開いた関数makeが、関数makeSandwichの変数名対応表(のようなもの)とセットになることで、それ以上、外側のスコープに変数を探しにいかなくてよくなります 。

ある種、完結したものになるのです。これを「閉じた」と表現しています。

クロージャによる関数定義

クロージャによる関数定義は、関数式 でも記述できます。

関数makeSandwich内部の関数(先ほどまでの関数makeのような)を個別に呼び出すことがないので、関数に名前をつける必要がありません。

また、さきほどまでローカル変数だった変数vegetableを関数makeSandwichの仮引数に設定し、変数に代入するときに引数として渡すことで、オブジェクトのようなものを生成できる ことがわかります。

function myFunction2_11_05() {

  function makeSandwich(vegetable) {
    return function (topping) {
      return vegetable + 'and' + topping;
    };
  }

  const lettuceAnd = makeSandwich('レタス');
  console.log(lettuceAnd('トマト')); //レタスandトマト
  console.log(lettuceAnd('バジル')); //レタスandバジル

  const eggAnd = makeSandwich('エッグ');
  console.log(eggAnd('トマト')); //エッグandトマト
  console.log(eggAnd('バジル')); //エッグandバジル

}

クロージャは、外側の変数を更新できる

クロージャは、外側の変数vegetableへの参照を保存します。決して値をコピーするわけではありません。

したがって、変数vegetableの更新は、スコープチェーンによりアクセスされる すべてのクロージャに反映 されます。

下記のような、3つのクロージャ(set、get、type)を用意しました。

変数vegetableへのアクセスは、クロージャ経由でなければなりません。

function myFunction2_11_06() {

  function makeSandwich() {

    let vegetable = undefined;
    const obj = {

      set(newVegetable) {
        vegetable = newVegetable;
      },

      get() {
        return vegetable;
      },

      type() {
        return typeof vegetable
      }

    };
    return obj;
  }

  const sw = makeSandwich();
  console.log(sw.type()); //undefined

  sw.set('レタス');
  console.log(sw.get()); //レタス
  console.log(sw.type()); //string

}

みなさんも、お気付きだと思いますが、これはgetter/setterです。

JavaScriptでgetter/setterが書けるのは、クロージャのおかげです。

そして、なんとなく、クロージャがクラスなんじゃないか と思われたかもしれません。さすがです。

JavaScriptのクラスは、クロージャ と、prototype によクラスと同様の書き方ができます。

function myFunction2_11_07() {

  //クロージャによるクラス
  function VegetableA(vegetable) {
    this.getVegetable = function () {
      return vegetable;
    }
    this.makeSandwich = function (topping) {
      return vegetable + 'and' + topping;
    }
  }

  //prototypeによるクラス
  function VegetableB(vegetable) {
    this.vegetable = vegetable
  }

  VegetableB.prototype.getVegetable = function () {
    return this.vegetable;
  }

  VegetableB.prototype.makeSandwich = function (topping) {
    return this.vegetable + 'and' + topping;
  }

  const a = new VegetableA('レタス');
  console.log(a.getVegetable()); //レタス
  console.log(a.makeSandwich('トマト')); //レタスandトマト

  const b = new VegetableB('レタス');
  console.log(b.getVegetable()); //レタス
  console.log(b.makeSandwich('チーズ')); //レタスandチーズ

}

しかし、クロージャによるクラスの書き方は、インスタンスを生成するたびにメソッド(もいっしょにコピーしてしまうので)の分までメモリを食ってしまいます。

prototypeの方が合理的でしょう。

そして、ES6からは クラス構文 が登場しました。

クラス構文では、constructorキーワードでコンストラクタ関数 を定義し、メソッド構文で関数(メソッド) を定義します。

なので、クロージャ(関数の中に関数がある)という書き方をあえて選択する場面はないと思います。JavaScriptの関数の成り立ちを理解しましょう。

function myFunction2_11_08() {

  class Vegetable {
    constructor(vegetable) {
      this.vegetable = vegetable;
    }

    getVegetable() {
      return this.vegetable;
    }

    makeSandwich(topping) {
      return this.vegetable + 'and' + topping;
    }
  }

  const v = new Vegetable('レタス');
  console.log(v.getVegetable()); //レタス
  console.log(v.makeSandwich('トマト')); //レタスandトマト

}

最後に、クラス構文によるgetter/setterもご紹介します。

function myFunction2_11_09() {

  class Vegetable {
    constructor(vegetable) {
      this._vegetable = vegetable;
    }

    get vegetable() {
      return this._vegetable;
    }

    set vegetable(vegetable) {
      this._vegetable = vegetable;
    }
  }

  const v = new Vegetable('レタス');
  console.log(v.vegetable); //レタス →getterの呼び出し
  v.vegetable = 'にんじん'; //setterの呼び出し
  console.log(v.vegetable); //にんじん

}

まとめ

以上で、「クロージャと仲良くしよう」をお届けしました。

テキストで書かれていたことは、クロージャは3つの事実があるということでした。

  • JavaScriptは、現在の関数の外側で定義された変数を参照できる
  • 関数は、その外側の関数の処理が抜けた後でも、まだ外側の関数内で定義された変数を参照できる
  • クロージャは、外側の変数を更新できる

わたしは、とくに2番目が理解できなかったので、「メモリの解放」という視点から考察してみました。

気付いてみると、「JavaScriptの関数はクロージャである」 という言葉の意味が、理解できた気がします。

これから、積極的にクラス構文を書いていこうと思います。

次回は、「変数の巻き上げ(ホイスティング)を理解する」 をお届けします。

参考資料

このシリーズの目次

タイトルとURLをコピーしました