MENU

【Javascript】クロージャーについて知る(勉強会開きました)

2021 6/19
【Javascript】クロージャーについて知る(勉強会開きました)

おつかれさまです。

本日はクロージャーについての記事になります!
どうぞよろしくお願いいたします。

今回の記事のテーマをもとに、私が在籍している「もりけん塾」というコミュニティで勉強会を開かせていただきました。

資料はこちら↓

クロージャーを苦労(クロー)せずに理解する

目次

前提知識

Javascriptのクロージャーを知る上で、まずはJavascriptの「スコープ」について知る必要があります。

スコープ

スコープとは変数の名前や関数などの参照できる範囲を決めるものです。 スコープの中で定義された変数はスコープの内側でのみ参照でき、スコープの外側からは参照できません。

JS Primer

スコープとはざっくり変数を参照することができる影響範囲のようなイメージを指します。

また、Javascriptではスコープは「グローバル変数」「ローカル変数」という2つの概念があります。

ローカル変数というのはある関数内で定義した変数のことを指します。

グローバル変数とは、どの関数やにも属さない変数のことで、基本的にどこからでも参照することができます

冒頭の説明にもつながりますが、基本的に関数の外部から関数の中にある変数にアクセスすることはできません。

要するに内側から外側は変数を見に行けるけど、外側からは内側には見に行けないよーっていうイメージです。
▶︎ 例えるならマジックミラー的な。内側からは見えるけど外側から見えないアレ。

変数にはグローバルが存在するということを説明しました。

ここで一つの疑問が生まれます。

function hoge(){
  const x = 0;
  console.log(x);
}

const x = 10;

hoge();

上では、どちらが まずhoge()を定義してから、後でhogeを実行します。

変数xはグローバルとローカルの両方で定義されています。

この場合、関数hogeを実行したとき0と10、一体どちらがログに表示されるでしょうか?

正解は、0になります。(よかったら実際に確認してみてください)

それはJavascriptがレキカルスコープ(静的スコープ)というスコープの性質を持っているからです。

レキシカルスコープ

慣れない言葉が出てきましたね。少しづつ紐解いていきましょう。

レキシカルスコープとは、一言でいうと

関数を定義した時点でスコープが決まること

を指します。

先の例で言うと

function hoge(){
  const x = 0;
  console.log(x);
}

ここで関数hogeを定義しましたが、ここのconsole.log(x)を実行すると、関数内で定義した変数xを読みにいきます。

また、外部で再びxを定義し直してもhoge内にある、変数xはなんら影響を受けません。

これがいわゆるレキシカルスコープ(静的スコープ)になります。

Javascriptやその他の言語(Ruby、Java、python等)ではこのスコープの仕組みを採用しています。

ダイナミックスコープ

一方でダイナミックスコープ(動的スコープ)と言う概念がありますが、これは

関数を実行した時点でスコープが決まります

はて、どういうことでしょうか。

先程の例をダイナミックスコープで置き換えると以下になります。

function hoge(){
  const x = 0;
  console.log(x);
}

const x = 10;
hoge(); //このときにスコープが再形成される

// → 10が表示される

先ほどのレキシカルスコープだとxの値は0でしたが、ダイナミックスコープだと、実行時点でスコープが決まるので、この場合は10をログに表示することになります。

Javascript以外の言語(Perlなど)ではこのダイナミックスコープと呼ばれる仕組みが採用されています。

出生による国籍取得の考え方を用いるとレキシカルスコープとダイナミックスコープの違いが理解しやすいかも?

血統主義:生まれた土地関係なく、父または母の国籍が与えられる
→ レキシカルスコープ(関数を定義した時点、つまりは生みの親のスコープで決まる)

出生地主義:生まれた土地によって国籍が与えられる
→ ダイナミックスコープ(生みの親とは関係ない)

さて、レキシカルスコープ(静的スコープ)ダイナミックスコープ(動的スコープ)に触れたところで、いよいよ本題のクロージャの話題に移ります。


クロージャーとは?

クロージャーは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。言い換えれば、クロージャは内側の関数から外側の関数スコープへのアクセスを提供します。

MDNより

多分初見でこれを読んで、理解できる人はほとんどいないと思います。

端的に言い表すなら

外側のスコープの関数内にある変数へアクセスできる内部関数のこと

を指します。

論より証拠。実際にコードを見て見ましょう。

よくあるカウントアップのサンプル。

function createCountUp() {
  let num = 0;
  function countUp() {
    num++;
    console.log(num);
  }
  return countUp;
}

const counter = createCountUp();

counter(); // 1
counter(); // 2
counter(); // 3
.
.
.

countUp関数を持つ変数counterを関数実行するたびに、0という数値が1ずつ加算されていることがわかります。それだけ。

今回でいえばここの関数(関数crateCoutUp内)

  function countUp() {
    num++;
    console.log(num);
  }

関数countUpから見たときに変数numというのはレキシカルスコープによりcountUpから参照されます。

つまりこの条件を満たしたときに、関数countUpはクロージャーとして機能することとなります。

具体的にどんなことが起きているかというと、

function counter() {
  let num = 0;
  function countUp() {
    num++;
    console.log(num);
  }
  return countUp;
}

で関数counterを定義。

関数counter内では新たに関数countUpを定義し、それをcounterという変数として返しています

そして、以下を実行。

const counter = createCountUp();

counter(); // 1
counter(); // 2
counter(); // 3

上ではcreateCountUp()を実行するので、countUp関数を返します。そしてそれをcounter変数にそのまま代入しています。

変数counterには createCountUp()の中のcountUp関数がそのまま代入されるので、counterに関数実行演算子()をつけることで代入された関数を実行することができるようになりました。

そして、counter()を実行するたびに変数numの値が1ずつ増加していっていることがわかります。

気づいた方もいるかもしれませんが、クロージャーをというのは関数の中の状態(変数)を保持する性質を持っています。

今回の例だと、counter()関数を実行する度に let num = 0の値が保持されて、実行するたびに値が1ずつ増えていることがわかります。

ざっくりこの現象が関数のクロージャという仕組みになります。

クロージャのメリット

さて、さらっとクロージャに関する説明をしましたが、これを使うことで一体どのようなメリットがあるのでしょうか。

色々調べたところ、多かったのは以下のメリットになります。

  1. グローバル変数の使用回避、擬似的なプライベート変数の作成を実現
  2. 関数に状態を持たせられる
  3. 高階関数の作成

1. グローバル変数の使用回避、擬似的なプライベート変数の実現

関数のクロージャを利用することで、擬似的にプライベートな変数を作成することができます。

もとより、Javascript以外の他のプログラミング言語の変数にはPrivatePublicなる概念がします。

ここでは本節と逸れますので、深くは突っ込まないですが要はクラスなどを経由しないとアクセスができないのが、プライベートな変数といいます。

パブリックはその逆で、プロジェクト内のどこからでも値の操作ができてしまう変数の概念になります。

Javascriptには上のようなプライベート、パブリックといった変数の概念は存在しません。

let cnt = 0;
 
function countUp() {
    cnt += 1;
    console.log(cnt);
}
 
countUp(); // 「1」が出力される
countUp(); // 「2」が出力される
countUp(); // 「3」が出力される

上では変数cntがグローバルに定義されていて、関数countUpcntの値を1加算して、ログに出力をしています。

これは至って普通の挙動ですよね。

ですが、今のこの状態では変数cntをどこからでもいじれる状況になっています。いわばグローバルな変数になります。

もしcountUpの途中で変数cntの値が書き換えられたとしましょう。

let cnt = 0;
 
function countUp() {
    cnt += 1;
    console.log(cnt);
}
 
countUp(); // 1

cnt = 10; // cntを10に書き換えた

countUp(); // 11
countUp(); // 12

これでは2回目の関数実行時点でcntの値が11になってしまいますよね。

要はこの変数がどこからでもいじられる状態というのが好ましくありません。

上では変数cntの中身は何の意味も表さないただの数値でしたが、これがパスワードや重要な情報だと、外部の影響によって簡単に値が書き換えられるのは良くないですよね。

それに、変数を無闇に書くことはJavascriptの世界においては「アンチパターン」※とされています。

※いわゆるダメな例的なやつです。上だと仮に、複雑なアプリケーションのようなものを作ろうとした場合、知らず知らずにグローバル変数で埋め尽くされてしまい、コード量が増えるにつれ変数名の競合が起こりやすくなってしまいます。(名前空間の汚染)

ここでクロージャーの登場です。上のコードをクロージャーとして書き換えると以下のようになります。


function createCountUp() {
  let cnt = 0;
    function countUp() {
      cnt += 1;
      console.log(cnt);
    return countUp;
  }
}
 
const counter = createCountUp(); // return countUp()

counter(); // 1
counter(); // 2
counter(); // 3

変数cntcreateCountUpの中で定義されたことにより、外部から変数cntにアクセスができなくなりました。

今、変数cntに直接アクセスができるのは、countUp関数を持っている変数counterだけになります。

こうすることで先のようなふとした拍子に変数の書き換えが起きるということもないですし、グローバル空間の汚染も防止することができます。一石二鳥です。

このようにクロージャーをうまく使うことで、グローバル変数を減らし、よりスマートで、かつメンテナンス性の高いコードを書くことができます。

2. 関数に状態を持たせることができる

createCountUp関数でもあったようにクロージャーを利用することによって、静的スコープで参照される変数の状態を保持することができます。

使用例:

jQuery(function($){
  var isClicked = false;
  $('.btn').click(function(){
    if (isClicked) {
      console.log('クリック済みです');
    }
    isClicked = true;
  });
});

よくサンプルとして挙げられる、関数にクリックした状態を保持させるといったもの。

親の関数の中にある関数がクロージャーとして動作。これにより変数isClickedの状態は保持され、ボタンのクリック動作を2回目以降で制限することができる。

3. 高階関数の一部として機能する

function createCalcTax(tax) {
  // 消費税の計算をする関数
  return function (price) {
    return price * tax;
  };
}

// 消費税10%に設定。
const calcTax1 = createCalcTax(1.1);
const calcTax2 = createCalcTax(1.08);

// createCalcTax関数内の無名関数の引数、priceに100を設定。
calcTax1(500); // -> 550
calcTax2(500); // -> 540

上では税込計算ができる関数をサンプルにしています。

const calcTax1 = createCalcTax(1.1);

の部分であらかじめ消費税の税率をcreateCalcTaxの引数として1.1に設定します。

createCalcTaxの引数taxは、変数として内部の無名関数により参照を受けるので、ここでクロージャーが成立することとなります。

そして、新たにcreateCalcTaxを実行後に、calcTax1という変数名に代入。calcTax1では無名関数の引数にあたるpriceを設定し実行することで、税込計算の処理が実現します。

また、別の変数に関数createCalcTaxを代入してやることで、calcTax1とは別のものとして新たに関数を作成することができます。(ここでは1.08としています)

ちなみに関数の引数や戻り値に関数を利用した関数のことを「高階関数」と呼びます。

EX:変数の値が保持されるのはナゼ?(メモリ管理の仕組み)

変数の値が保持されるという理由にJavascriptのメモリ管理の仕組みが関わってきます。

プログラミング言語では、使わなくなった変数やデータを取り除いて、「メモリ」を解放する仕組みを持っています。

メモリ:変数や関数を定義した時にデータを確保するための領域

Javascriptではこの不要になったデータを検知し、自動的にメモリから除去する仕組みが採用されています。これを「ガベージコレクション(GC)」と呼びます。

let x = "before text";
// 変数`x`に新しいデータを代入する
x = "after text";
// このとき"before text"というデータはどこからも参照されなくなる
// その後、ガベージコレクションによってメモリ上から解放される

上記のコードはメモリ解放を表すためのサンプルコードです。

例えばここで、文字列"before text"というデータが変数xに代入されていますが、そのあと変数xには"after text"という新たな文字列が代入されます。

最初にメモリ上に確保されていた文字列”before text”というデータは変数xに新しく"after text"という文字列が代入された時点で、どこからも参照されなくなるため、不要なデータとなります。

Javascriptでは不要になったデータはガベージコレクションという仕組みによって自動的にメモリから解放されます。

不要になったデータはゴミ箱へ

繰り返しにはなりますが、不要になったデータというのは処理の実行後にどこからも参照されなくなったデータを指します。

function logArray() {
  const tempArray = [1, 2, 3];
  console.log(tempArray);
}

logArray(); // [1,2,3]

上のコードは関数の処理実行後に、参照されなくなったデータを有した関数とその実行を表しています。

この関数は定義した変数tempArrayをログに出力しますが、実行後に配列[1,2,3]を参照するということはしません(ログに出力しているだけ)。

▶︎ これにより配列[1,2,3]は不要なデータとなるため、メモリ上から解放されます。

次に、ガベージコレクションによってメモリからデータの解放がされない例です。

function createArray() {
    const tempArray = [1, 2, 3];
    return tempArray;
}
const array = createArray();
console.log(array); // => [1, 2, 3]
// 変数`array`が`[1, 2, 3]`という値を参照している -> 解放されない
  1. 関数createArray は文字列を参照した変数tempArray を返します。
  2. 関数createArrayを実行したものを新たに変数arrayに代入されます。
  3. 配列オブジェクト[1,2,3]は、createArray関数の実行終了後も変数arrayから参照され続けています。

▶︎ 関数の実行後に一つでも参照を受けているならば、GCによるメモリ解放を受けません。

さて、色々と前置きが長くなってしまいましたが、少し正解に近づいてきたのではないでしょうか。

先のクロージャーのサンプルをもう一度見てみましょう。

function createCountUp() {
  let cnt = 0;
    function countUp() {
      cnt += 1;
      console.log(cnt);
    return countUp;
  }
}
 
const counter = createCountUp(); // return countUp()

counter(); // 1
counter(); // 2
counter(); // 3
  1. createCountUp関数を定義します。
    (レキシカルスコープによりcountUp内のcnt変数は外側のcntを参照。)
  2. createCountUp関数を実行後に、変数counterによって参照されます。 (let cnt = 0 を参照したcountUp関数をcounter変数が参照するので、変数cntはメモリの解放を受けない。)
  3. counter変数を実行することで値をインクリメントできる。
    ▶︎  2.で変数cntがメモリ解放を受けなったので、変数cntの値は蓄積される。

2.のデータの参照の流れを表すとこうなります。

変数 counter関数countUp変数cnt

という流れで変数cntへの参照が続いているため、変数の状態が保持される。これがクロージャーによって値が保持されている仕組みになります 。

メモリ管理に関して、もっと詳しく知りたいという方はこちらの一読をお願いします。

実装サンプル

①クリックした状態を保持する。

  • フォームとかで無限にリクエストが送れてしまうことを防ぐことができる
  • flagという変数を外部から書き換えることをできなくする。

See the Pen ExWMYeL by rik9228 (@rik9228) on CodePen.


②:一意のIDを振った名簿リストの作成

See the Pen qBrLwKV by rik9228 (@rik9228) on CodePen.

createPerson("佐藤", 25, "男");

という感じに引数に、

  • 名前
  • 年齢
  • 性別

を指定して実行することで、簡単なプロフィールリストが作成できる。『NO.』(変数num)は クロージャによって値が保持されるので、自動的に1ずつ加算されたものが表示されるようになる。

まとめ

ここまでいかがだったでしょうか。

少し堅苦しい話が続きましたが、まとめると以下になります。

■クロージャとは
 → 外側のスコープの関数内にある変数へアクセスできる内部関数のこと

■メリット
 ・グローバル変数の節減ができ、かつ変数を外部から変更されないようにすることができる。
 ・関数に状態を持たせることができる
 ・高階関数の一部として機能する

★クロージャが動作する背景にはJavascriptの「メモリ管理」が背景にある。

ここまでまとめておいてなんですが、割と難しいテーマだった気がします😅

記事を書いてみて感じたのはクロージャが大切というより、結局、関数の仕組みを知ることが大切だなあと思いました。

実際にあるのは制作(jQueryとかが主になっている現場)の現場ではクロージャーを利用することがそれなりにあるようで、

開発の現場(フレームワークなどを使う現場)だとおそらくストアで変数管理をする理由だったりするためか、制作現場よりかは使う機会は少ないそうです。とはいえ、全く使わないことはないそうですが。

とはいえ、繰り返しにはなりますが、改めて関数の仕組みを知ることができた良い機会になりました。

それではまた次回の記事でお会いしましょう〜〜〜


もりけん塾に入っております!(Javascriptを鋭意勉強中)

もりた先生のブログ: kenjimorita.jp (武骨日記)
もりた先生のアカウント: twitter.com/terrace_tech

参考

あわせて読みたい
No Image
関数とスコープ · JavaScript Primer #jsprimerスコープという変数などを参照できる範囲を決める概念を紹介します。ブロックスコープや関数スコープなどがどのような働きをしているのかや複数のスコープが重なったときに...
WEMO
【JavaScriptの基礎】レキシカルスコープとクロージャを理解する | WEMO
【JavaScriptの基礎】レキシカルスコープとクロージャを理解する | WEMO「JavaScriptを理解する」第3回です。今回は「クロージャ」というものをメインに勉強してきました。 また、クロージャを理解するためには、JavaScriptで採用されているスコ...
あわせて読みたい
クロージャ - JavaScript | MDN
クロージャ - JavaScript | MDNクロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。言い換えれば、クロージャは内側の関数から外側の関数スコ...

この記事を書いた人

コメント

コメントする

CAPTCHA


目次
閉じる