クロージャについてふんわり理解する

忘備録を書いた理由

  • JSを書いているといきなり無名の関数が出てきてなんぞやとなった。
  • クロージャ」が一体何の役割を持っているのかよく分からない。
  • Wikipediaをチラ見すると難しそうなことが書かれてて理解するのに時間かかりそうだから飛ばした。
  • 「無名関数=クロージャ」等と一部のサイトでは書かれてて、それが正しくないとの意見を見かけたので調べたくなった。

クロージャとは

クロージャ、関数閉包はプログラミング言語における関数オブジェクトの一種。いくつかの言語ではラムダ式や無名関数にて利用可能な機能・概念である。引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。 クロージャ - Wikipedia

これだとよく分からないので、この中に登場したよく分からない単語を一旦調べてみる。

関数閉包とは

単にクロージャという単語を日本語訳したもの。 「クロージャ = 関数閉包」

ラムダ式

ラムダ式 (lambda expression) はラムダ計算と関係が深く、関数型言語で特によく採用されている。

ラムダ計算とは...

ラムダ計算

ラムダ計算は、計算模型のひとつで、計算の実行を関数への引数の評価と適用としてモデル化・抽象化した計算体系である。ラムダ算法とも言う。関数を表現する式に文字ラムダ (λ) を使うという慣習からその名がある。 ラムダ計算 - Wikipedia

難しいことは端折る。つまりこういうことだと思う。

  • 「ラムダ計算」はプログラミング界で発明された計算方法ではなく、元々は数学で使われている計算式。

  • その「ラムダ計算」がプログラミングの世界(関数型言語)でも使われていて、その計算方法を応用した書き方を「ラムダ式」と呼ぶ。

以下の記事は細かくまとめられていて勉強になりました↓ qiita.com

無名関数

プログラミング言語における無名関数とは、名前付けされずに定義された関数のことである。無名関数を表現するための方法には様々なものがあるが、近年主流となっているのはラムダ式による記法である。無名関数を表現するリテラル式は、関数リテラルとも呼ばれる。 無名関数 - Wikipedia

シンプルに名前がない(関数名がついていない)関数のことを指す。 外部から呼ばれることがない場面や、一度きりの使用の時に限り利用されている。
Javascriptだと、forEachやfilter等が引数に無名関数を使ったメソッドに挙げられます。

普通の関数

function hello(name) {
  return `${name}さん、こんにちは!`;
}

無名関数

const hello = function(name) {
  return `${name}さん、こんにちは!`;
}

静的スコープ

どの識別子がどの変数を参照しているかを静的に決定する性質。
静的スコープ - Wikipedia

const hoge = 'こんにちは'; // *1

function hello() {
    // この識別子`hoge`は常に *1 の変数`hoge`を参照する
    console.log(hoge); // => こんにちは
}

以下の例だと、 「console.log(hoge);」の変数hogeは常にグローバルスコープで定義されたhogeを参照する。 動的スコープだとコードの内容によっては参照先が変わるみたいです。ちなみにJavascriptは静的スコープを持つ言語です。

用語を意味をまとめると

  • 関数閉包
  • ラムダ式
    • ラムダ文字を使った計算方法の応用
  • 無名関数
    • 名前のない関数
  • 静的スコープ
    • どの識別子がどの変数を参照しているか決定する性質

用語単体の知識はついたものの結局クロージャが何なのかわからない...。
ここでMDNでクロージャのについて書かれたページを見てみる。

改めてクロージャとは

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

サンプルコードを読み解いていくと分かりやすいです。

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

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

init()の中で宣言されている変数nameがdisplayName()の中でも使えているのがクロージャのポイント!

クロージャは、内側から自分を括っている外側のスコープにアクセスすることが可能です。
上記のプログラムの中だと、クロージャに当たるのはdisplayName()になります。

また、ネストが深くなっても全ての外側のスコープにアクセスできます。

// グローバルスコープ
var e = 10;
function sum(a){
  return function(b){
    return function(c){
      // ローカルスコープ
      return function(d){
        // ローカルスコープ
        return a + b + c + d + e;
      }
    }
  }
}

console.log(sum(1)(2)(3)(4)); // log 20

まとめ

  • クロージャとは、自分自身が定義された環境でその周囲の要素が使える機能をもったもの
  • ネストが深くなっても外側のスコープの要素が使える