クロージャについて勉強した

はじめに

クロージャについて友人と勉強会を行ったので、学んだことや理解したことをまとめておきたいと思います。

対象読者

  • クロージャについて雰囲気だけでも知っておきたい人
  • 初心者がどのようにクロージャを学んでいるのか気になる人

クロージャについてしっかり説明してる記事ではないので、その点はご了承下さい。

また、説明において間違ってる箇所がありましたら、教えて頂けると幸いです。

もくじ

クロージャよくわからない

クロージャについてMDNの説明をみてみます。

クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。

クロージャ - JavaScript | MDN

組み合わされた(囲まれた)関数?

その周囲の状態(レキシカル環境)?

???

よくわからないですね。。。

一つずつ理解していくことにしました。

組み合わされた(囲まれた)関数って何?

よくわからないので、MDNでコード例をみてみました。

function init() {
  var name = 'Mozilla'; // name は、init が作成するローカル変数

  function displayName() { // displayName() は内部に閉じた関数
    alert(name); // 親関数で宣言された変数を使用
  }
  displayName();
}
init();

クロージャ - JavaScript | MDN からコード例を引用)

上の例でいうと、init関数のなかに、displayName関数が定義されています。(※ JavaScriptは関数の中に関数を定義できます。)

displayName関数はinit関数に囲まれていますね。

つまり、関数の中に関数がある状態のことを「組み合わされた(囲まれた)関数」と表現しているようです。

レキシカル環境って何?

これもよくわからないので、検索して出てきたWikipediaの説明をみることにしました。

静的スコープ(せいてきスコープ、英: static scope)とは、プログラミング言語におけるスコープの一種。字句のみから決定できるため、字句スコープまたはレキシカルスコープ (lexical scope) ともいう。

静的スコープ - Wikipedia

Wikipediaによると、レキシカルスコープは静的スコープとも言うようです。

静的スコープについてコード例を用いて理解することにしました。

静的スコープの例

下記は静的スコープを説明したコード例です。

// 静的スコープの例
const x = 1;
const hoge = () => {
  console.log(x);
};

const piyo = () => {
  const x = 3;
  hoge();
};

piyo(); // xの値は1または3どちらが出力される?

上記のコードの1行目では変数xがグローバル変数として定義されています。

piyo関数の中で変数xがローカル変数として定義されていて、処理の中でhoge関数が呼び出されています。

ここでpiyo関数を実行すると、変数xの値は1または3どちらが出力されるでしょうか?

const x = 1;
const hoge = () => {
  console.log(x);
};

const piyo = () => {
  const x = 3;
  hoge();
};

piyo(); // 1

実行すると、xの値は1が出力されています。つまり、グローバル変数xの値が参照されているということになります。

よって、hoge関数が実行された環境ではなく、定義された環境で、変数xの値が決まっていることがわかります。

静的スコープとは、定義された時点でスコープが決まるという概念ぽいです。

ちなみに、変数xへの参照はスコープチェーンによって決まっています。

スコープチェーンについて → https://jsprimer.net/basic/function-scope/#scope-chain

逆に、動的スコープという概念もあるそうです。動的スコープなら、hoge関数が実行された環境で変数xの値が決まります。

// JavaScriptが動的スコープを採用してる場合こうなる(擬似的なコード)
const x = 1;
const hoge = () => {
  console.log(x);
};

const piyo = () => {
  const x = 3;
  hoge();
};

piyo(); // 3

動的スコープが使用されてるプログラミング言語もあるらしいのですが、JavaScriptは静的スコープが使用されてるようです。

まとめると、レキシカルスコープとは、静的スコープの別名で、定義時にスコープが決定するものという理解でよさそうです。

で、クロージャってなんなの?

言葉の説明だけではイメージしずらいということで、コード例を用いて理解していくことにしました。

下記のコードは、クロージャが使われている例です。

const counter = () => {
  let count = 0;
  const innerCounter = () => {
    count += 1;
    return count;
  };
  return innerCounter;
};

const myCounter = counter();
console.log(myCounter()); // 1
console.log(myCounter()); // 2

counter関数の中にinnerCounter関数があります。

counter関数は戻り値として内部のinnerCount関数自体を返します。

innerCount関数内の処理としては、呼び出されるたびに、変数countの値が+1されるというものです。

よくみると、innerCounter関数内で外側の関数への環境にある変数countを参照しています。

この「innerCounter関数からcounter関数内の環境への参照の組み合わせのこと」(今回で言うと、innerCounter関数と変数countの関係のこと)をクロージャと呼んでいます。

ここで再度MDNのクロージャの定義を確認してみます。

クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。

今までの理解を踏まえて、上記の説明について自分はこう解釈しました。

クロージャとは、「内部にある関数からその外側の関数の環境への参照の組み合わせのこと」である。

クロージャの特徴

クロージャのイメージが少し掴めたところで、今度はクロージャの特徴をみてみることにしました。

特徴としては大きく2つありました。

  1. 参照される外側の環境にある変数はプライベートな変数として扱うことができる
  2. 参照される外側の環境にある変数の値は、参照され続ける限り値が保持される

先ほどのコード例でみていきたいと思います。

1. 参照される外側の環境にある変数はプライベートな変数として扱うことができる

1つ目の特徴についてです。

const counter = () => {
  let count = 0;
  const innerCounter = () => {
    count += 1;
    return count;
  };
  return innerCounter;
};

const myCounter = counter();
console.log(myCounter()); // 1
console.log(myCounter()); // 2

// 外部から変数countの値は変更できない
// JavaScriptは静的スコープなので、innerCounter関数が参照する変数countは定義時にcounter関数の環境にある変数countであると決まる
const count = 100;
console.log(myCounter()); // 3

この例でいうと変数countをプライベートな変数として扱うことができるということです。

というのも、変数countの値は外部から変更することはできなくなっています。

これは静的コープの理解にもつながりますね。(定義時に参照先が決まる)

変数countの値を変更したい場合は、innerCounter関数を実行するしかないのです。ゆえに、グローバル変数を使用しない安全なコードにできるという利点があります。

2. 参照される外側の環境にある変数の値は、参照され続ける限り値が保持される

続いて2つ目の特徴についてです。

同じく先程のコードでみていきたいと思います。

const counter = () => {
  let count = 0;
  const innerCounter = () => {
    count += 1;
    return count;
  };
  return innerCounter;
};

const myCounter = counter();
console.log(myCounter()); // 1
console.log(myCounter()); // 2

// 参照先を変えると変数countの値は別で保持される
const otherCounter = counter();
console.log(otherCounter()); // 1
console.log(otherCounter()); // 2
console.log(otherCounter()); // 3
console.log(otherCounter()); // 4

console.log(myCounter()); // 3

上記のコードの変数myCounterには、counter関数の実行内容つまり、innerCounter関数自体が入っています。

変数countはinnerCounter関数内から参照されています。つまり、変数myCounterによってinnerCounter関数が参照され続ける限り、変数countの値も参照され続けることになります。

そのため、myCounter()を実行する度に、値が1ずつ増えていくことになるということです。

また、参照先を変えると変数otherCounterが持っている変数countの値は、変数myCounterがもっている変数countの値とは別で保持されます。

以上より、クロージャには大きく2つの特徴があることが分かって頂けたと思います。

続いてはクロージャが使われてるコード例をいくつか紹介したいと思います。

ちょっと補足

クロージャのコード例は即時実行関数を使って書かれていることが多いです。

先ほどのコード例を即時実行関数を用いて書くと、以下のようになります。

const counter = (() => {
  let count = 0;
  const innerCounter = () => {
    count += 1;
    return count;
  };
  return innerCounter;
})();

const myCounter = counter;
console.log(myCounter()); // 1
console.log(myCounter()); // 2

クロージャのコード例をみると度々使われているので、少し補足で紹介しておきました。

クロージャを使った他の例

クロージャを使った他の例を簡単にですが紹介しようと思います。

メソッドを使ってオブジェクト指向っぽい使い方ができる

下記のコードではreturn以下のincrementメソッド内とdecrementメソッド内で変数countが参照されています。

つまり、変数countとincrement, decrementメソッドの関係もクロージャと言えます。

こののような実装にすることで、クラスを使わずに関数が状態をもつように振る舞わせることができます。(オブジェクト指向っぽい使い方が可能になる)

const counter = () => {
  let count = 0;
  return {
    increment: () => {
      count += 1;
      return count;
    },

    decrement: () => {
      count -= 1;
      return count;
    },
  };
};

// オブジェクト指向っぽく使用できる
// 状態をもつ & 外部から変数が変更されない

const myCounter = counter();
console.log(myCounter.increment()); // 1
console.log(myCounter.increment()); // 2
console.log(myCounter.increment()); // 3
console.log(myCounter.decrement()); // 2

const otherCounter = counter();
console.log(otherCounter.decrement()); // -1
console.log(otherCounter.decrement()); // -2

関数とスコープ · JavaScript Primer #jsprimer からコード例を引用)

カリー化もクロージャが使われてる

JavaScriptにはカリー化というテクニックがあります。

下記のコードはカリー化の例です。

innerFunc関数内で変数xが参照されていますが、このinnerFunc関数と変数xの関係もクロージャの関係であると言えます。

const addFunc = (x) => {
  const innerFunc = (y) => {
    return x + y;
  };
  return innerFunc;
};

const addFive = addFunc(5); // 部分適用
const addSeven = addFunc(7); // 部分適用
console.log(addFive(10)); // 15
console.log(addSeven(10)); //  17

クロージャ - JavaScript | MDN からコード例を引用)

Rubyにもクロージャがある?

Rubyでもクロージャが使われてる例を学んだので紹介します。

Procオブジェクト作成する際に、ブロック内から変数countが参照されています。

このProcオブジェクトと変数countの関係がクロージャとなっています。

def counter
  count = 0
  Proc.new { count += 1 }
end

my_counter = counter # 変数my_counterでcounterメソッドを参照しておく
# 変数countの値を保持できる
puts my_counter.call #=> 1
puts my_counter.call #=> 2
puts my_counter.call #=> 3

Rubyにおけるクロージャについては、『プロを目指す人のためのRuby入門』(通称 チェリー本)でも紹介されていました。(p. 433 10.5.3 「Proc オブジェクトとクロージャ」)

気になる方はぜひ確認してみて下さい🍒

参考: RubyJavaScriptのスコープの違い

少し話はそれてしまいますが、RubyJavaScriptのスコープの違いについても勉強になったので紹介します。

Rubyではトップレベルの変数に対してメソッド内から変数の参照はできないのに対して、JavaScriptはそれが可能であるという言語の違いがあります。

当たり前かもしれないですが、自分はあまり意識したことがなかったのでハッとしました。

# Ruby
count = 0
def counter
  puts count
end

counter #=> undefined local variable or method `count' for main:Object (NameError)
# エラーになる
// JavaScript
let count = 0;
const counter = () => {
  console.log(count);
};

counter(); // 0
// 参照できる

クロージャまとめ

クロージャとは、「内部にある関数からその外側の関数の環境への参照の組み合わせのこと」

また、クロージャの特徴として以下の2つがあげられる。

  1. 参照される外側の環境にある変数はプライベートな変数として扱うことができる
  2. 参照される外側の環境にある変数の値は、参照され続ける限り値が保持される

わかってないこと

JavaScriptの関数は全てクロージャである。という記述をよく見るのですが、あまりピンときていないです・・

また、@haruguchi さんにクロージャを使ったキャッシュとメモ化の利用方法を教えてもらったのですが、理解できていないので書いていません。理解出来たら書こうと思います。

さいごに

きちんと理解できていない箇所もありますが、わかっている範囲でクロージャについてまとめました。

間違って説明している箇所などありましたら、教えて頂けると幸いです。

JavaScriptについてわからないことが増えたので、引き続き学習していきたいと思います。

参考