派遣で働くエンジニアのスキルアップを応援するサイト

PRODUCED BY RECRUIT

JavaScriptを学ぶ上でつまずきやすいポイントを理解するための連載【第2回】2つの利用シーンから理解する「クロージャ」活用法

JavaScriptでつまずきやすいトピックをとりあげて解説する、本連載。今回は「クロージャ」について。クロージャの仕組みは理解できていますか?あまり馴染みがない…。そんな方は、実際に活用できるシーンに気づくのも難しいこと。今回は、クロージャのよくある利用シーンをサンプルコードと共にご紹介します。これまで直感的に使っていたという方も改めて基本から学び直すと、予期せぬバグを防げるかもしれません。

【筆者】佐藤正志さん
契約社員、客員研究員、フリーランスなどさまざまな立場で開発現場を経験。フロントエンド、バックエンド開発、チームリードやOJTの育成担当まで幅広くこなしてきた。現在プログラミングトレーニング事業を行うサークルアラウンド株式会社を立ち上げ、経営や現場開発を行いながらOJT経験をもとに後進の育成に励んでいる。ノウハウのコンテンツ化にも取り組んでおり、著書に『ステップアップJavaScript』(翔泳社)がある。

*記事内に使用しているサンプルコードは、以下からも確認することができます。
https://github.com/CircleAround/engineer_style_js/blob/main/02/02_closure.js

そもそも「クロージャ」とは?

クロージャとは、ある関数からその外側の関数スコープへのアクセスを提供するものです。JavaScriptで関数を作成するとクロージャが形成されるので、関数が持つ性質のようにイメージすると理解しやすいでしょう。

コード例を読みながら確認してみましょう。

function createClosure() { //---(1)
  const value = "myClosureValue";

  function myClosure() { //---(2)
    // valueはmyClosureの外ではあるが、myClosureの親関数であるcreateClosure内にあるのでアクセス可能
    console.log(value);
  }
  return myClosure; //---(3)
}

const closure = createClosure(); //---(4)
closure(); // => "myClosureValue" // ---(5)

(1)のcreateClosure関数の中でさらに(2)のmyClosure関数を定義しています。myClosure関数の中にはローカル変数はなく、createClosure関数のローカル変数valueを利用しています。

(3)でcreateClosure関数の戻り値はmyClosure関数となっていますので、(4)のclosure変数はmyClosure関数を指しています。

このclosureを(5)のように関数として呼び出すと、(2)のmyClosureが動作することは想像できると思いますが、以下のような疑問を持つ方がいるかもしれません。

・myClosureの中からvalueにアクセスしているが、これはエラーにならない?
・closure変数を利用するときには、すでにcreateClosure関数は終了している。なので、value変数はすでに破棄されて、エラーになるのでは?

実際に動かすと懸念しているエラーは起こりません。myClosure関数がクロージャを形成するためです。

次の絵を見ながら確認してみましょう。クロージャによって、関数定義を行った場所からその外側の関数の変数、さらにその外側の変数…とさかのぼって上位のスコープの変数にアクセスできます。また、変数だけではなく関数なども同様にアクセス可能です。

次に、よくあるクロージャの利用シーンを見てみましょう。

ケース1:クロージャによるカプセル化

クロージャの利用シーンの一つとして、カプセル化(※後述)があります。関数内のローカル変数には外から直接アクセスできないので、オブジェクトの外から操作されたくない情報を守ることができます。クロージャを使用しない場合と使用する場合で比較してみましょう。

▼クロージャを使用しない場合
ガチャを引くような簡単なサンプルプログラムを用意しました。このコードはガチャを引くたびに当たりの確率が上がり、5回目以降は全部当たりになるような仕様です。machineオブジェクトを書いた人と、利用している人が別の人だと想像してください。

const machine = {
  count: 0,
  gacha() {
    this.count++;
    const rate = this.count * 0.2 + Math.random(); // countが増えると当たりやすくなります
    if (rate > 1) { // rateが1を越えれば当たりなので、countが5以上でずっと当たります
      console.log(`${this.count}回目で当たりが出ました! rate: ${rate}`);
    } else {
      console.log(`${this.count}回目はハズレです! rate: ${rate}`);
    }
  },
  clear() {
    this.count = 0;
  }
};

machine.gacha();
// machine.count = 5; //---(1)
machine.gacha();
machine.gacha();
machine.gacha();
machine.gacha(); // 通常ここでは必ず当たり

machineにとってcountはロジックの要になる値です。この値がgacha関数の呼び出しごとに一回ずつ増えているからこそ、仕様通りの動作ができます。

ここで誤って(1)のコメントのようにmachine.count = 5と直接代入してしまったとしましょう。そうすると、そのあとはgacha()関数は必ず当たりを出してしまいます。想定外の操作でバグを生み出してしまいました。

このようなオブジェクトの誤った操作によるバグを防ぐ方法として「countの値を外から操作させない」というカプセル化の概念を利用します。カプセル化を実現するために、クロージャは都合が良いのです。以下の修正コードをご覧ください。

▼クロージャを使用した場合

function createGachaMachine() {
  let count = 0; // これをプライベート変数として外から操作させたくない
  return {
    gacha() {
      count++;
      const rate = count * 0.2 + Math.random();
      if (rate > 1) {
        console.log(`${count}回目で当たりが出ました! rate: ${rate}`);
      } else {
        console.log(`${count}回目はハズレです! rate: ${rate}`);
      }
    },
    clear() {
      count = 0;
    },
  };
}

const machine = createGachaMachine();
machine.gacha();
// machine.count = 5; をしても、countの値は変更できません
...

count変数はcreateGachaMachine関数のローカル変数になっているので、gacha関数とclear関数だけがクロージャによってcount変数にアクセスすることができます。戻り値であるmachine変数を介して直接アクセスすることはできません。

これでガチャのコードは堅牢になり、間違って当たりばかりのガチャになってしまうようなことはなくなりますね。

使いこなせると便利!カプセル化
「カプセル化」とは、オブジェクト指向の文脈でよく取り上げられる考え方です。必要な情報だけをオブジェクトの外から操作でき、そうでない情報にアクセスさせないことです。内部で状態を保持している変数がアクセスできないよう隠蔽されることが多いです。

こうすることで、コードを書いた人の意図に反したオブジェクトの利用を禁止して、バグを減らすことができます。また、コードを変更する際に影響範囲がオブジェクト内に限定されるので、コードの変更コストが下がる効果もあります。

ポイント
クロージャでは、コードの文脈からカプセル化を主目的としているか判断しますが、クラスのアクセス修飾子を利用すると、カプセル化をより明示しやすくなります。ただし、参加するプロジェクトによってクラスの使用の程度が違う場合があります。プロジェクトの流儀に合わせて使用すると良いでしょう。

ケース2:関数を柔軟に生成する

クロージャの他の利用として「関数を動的に作り出す」といった内容もあります。こちらも実際にコードを比較してみましょう。

▼クロージャを使用しない場合

// funcは関数で、この関数を引数なしで呼び出した結果を利用する動作
// この関数がライブラリ等で提供されていて変更できないと仮定してください。
function output(func) {
  console.log(func()); // 引数なしで呼び出されると決まっている
}

// もしもクロージャを知らないと、関数を必要な分作る必要がある
function range1to3() {
  const values = [];
  for (let i = 1; i <= 3; i++) {
    values.push(i);
  }
  return values.join("|");
}

function range4to6() {
  const values = [];
  for (let i = 4; i <= 6; i++) {
    values.push(i);
  }
  return values.join("|");
}

output(range1to3); // => 1|2|3
output(range4to6);// => 4|5|6
// 必要になったらいくつも関数を作るのだろうか...

outputは「『引数のない関数』を引数に取る関数」です。このような関数が外部ライブラリから提供されている場合があります。ライブラリに合わせたコードを書くためには、仕様に合わせた関数を渡さなければなりません。また、クロージャを使わない場合には、必要な関数に名前を一つひとつ付けておく必要があります。

▼クロージャを使用した場合

// クロージャを利用すれば複数の関数を作らずに済む
function rangeString(begin, end) {
  return () => {
    const values = [];
    for (let i = begin; i <= end; i++) {
      values.push(i);
    }
    return values.join("|");
  };
}

output(rangeString(1, 3)); // rangeString(1, 3) は関数を戻り値にしている
output(rangeString(4, 6));

今回の場合にはbegin, endの引数を、戻り値となる関数のロジックからアクセスしています。結果、好きな値を引数に取って動的に動作が変わる関数を実現しました。「クロージャによって柔軟な関数作成が行える」と伝わったでしょうか。

今回は、駆使するのが難しい「クロージャ」について解説しました。特に、クロージャによるカプセル化は、大変便利な機能です。チームメンバーと一緒にコードを書く場合には、ぜひ活用してください。次回の解説は「非同期」について。うまく扱えないという声が多い「非同期処理」ですが、UIの使いやすさを考えると、必ず押さえておきたいトピックです。次回をお楽しみに。

▼これまでのJavaScriptを学ぶ上でつまずきやすいポイントを理解するための連載
【第1回】JS特有の「this」の使い方6つをおさえよう