11
14

クロージャは、JavaScriptでは非常に重要な概念の一つですが、初心者にとっては抽象的で難解に感じることがあります。特に、ECMA規格での定義は実務経験がないと理解しにくいでしょう。そこで、本記事ではクロージャの概念について長々と説明するのではなく、具体的なコードを通して短時間で理解できるようにします。

1. これがクロージャだ

新しい技術に触れる際に、まず最初にやるべきことは、そのデモコードを探すことです。私たちにとって、コードを読むことは自然言語よりも物事の本質を理解しやすいです。実際、クロージャは至るところで見られます。たとえば、jQueryZepto のコアコードは大きなクロージャに包まれています。では、まず最も簡単で基本的なクロージャを紹介します。これにより、クロージャの概念を頭の中にイメージしやすくなるでしょう。

function A(name){
    function B(){
       console.log(name);
    }
    return B;
}
var C = A("田所浩二");
C();//田所浩二

これは最も簡単なクロージャです。

これで基本的な理解ができたところで、通常の関数と何が異なるのかを簡単に分析してみましょう。上記のコードを自然言語に翻訳すると、以下のようになります。

  1. 通常の関数Aを定義する、引数はname
  2. A の中で通常の関数 B を定義する、関数Bでは外部変数nameを参照する
  3. A の中で B を返す
  4. A を実行し、その結果を変数 C に代入する
  5. C を実行する

この5つの操作を一言でまとめると、次のようになります。

関数Aの内部関数Bおよび変数nameが、関数Aの外部で変数Cによって参照されている。

この文を少し加工すると、次のようにクロージャの定義となります。

ある内部関数が、その外部関数の外にある変数によって参照されると、クロージャが形成される。

したがって、上記の5つの操作を行うと、クロージャが定義されるということになります。

これがクロージャだ。

2. クロージャの用途

クロージャの用途を理解する前に、JavaScriptのGC(ガーベジコレクション)メカニズムについて理解しましょう。

JavaScriptでは、オブジェクトが参照されなくなると、GCによって回収されます。そうでない場合、そのオブジェクトはメモリに保持され続けます。

上記の例では、B は A の中で定義されているため、B は A に依存しています。そして、外部変数 C が B を参照しているため、A は間接的に C によって参照されていることになります。

つまり、A は GC によって回収されず、メモリに保持され続けます。この推論を証明するために、上記の例を少し改良してみましょう。

function A(){
    var count = 0;
    function B(){
       count ++;
       console.log(count);
    }
    return B;
}
var C = A();
C();// 1
C();// 2
C();// 3

コード説明

  1. var C = A(); と呼び出すと、A が実行され、count 変数と内部関数 B が作成されます。AB を返すので、C 変数は実際には B の参照を持ちます。

  2. 関数 BA 内の count 変数にアクセスできます。なぜなら、B はクロージャであり、クロージャはその作成時のコンテキスト(ローカル変数など)を保持するからです。

  3. C() を呼び出すと、実際には B 関数が呼び出されます。C() を呼ぶたびに、Bcount の値を増加させ、その値をコンソールに表示します。B はまだ count 変数を参照しているので、count の値は増え続けます。

  4. A の実行コンテキストは B が作成された時点で終了しますが、B がそのローカル変数(count など)を参照している限り、A のローカル変数は回収されません。

  5. B が参照されなくなった時点で初めて、count 変数や A 内の他のローカル変数が回収されます。この例では、C がまだ B を参照しているため、count の値は回収されず、A の実行コンテキストも回収されません。

なぜ count はリセットされないのか

  • クロージャのメカニズム:クロージャによって count の状態が保持され、内部関数 B からアクセス可能な状態が維持されます。たとえ A の実行コンテキストが終了しても、B がこの状態を参照し続けるため、count の状態はメモリに残ります。
  • 毎回 B を呼び出す際:毎回 C() を呼び出すことは実際には B() を呼び出すことであり、B() はクロージャに保存されている count を使用し、再初期化することはありません。

したがって、モジュール内でいくつかの変数を定義し、これらの変数をメモリに保持しつつ、グローバルな変数を「汚染」しないようにしたい場合、クロージャを使ってこのモジュールを定義することができます。

3 LeetCodeでの実践

クロージャの理解を確認するために、LeetCodeの練習問題を通じて実践してみましょう!

リンク:https://leetcode.com/problems/counter/description/?envType=study-plan-v2&envId=30-days-of-javascript

//日本語翻訳

2620. カウンター
簡単
企業
ヒント
整数 n が与えられたとき、カウンター関数を返します。このカウンター関数は、最初は n を返し、その後は呼び出されるたびに前回の値より 1 多い値を返します(n, n + 1, n + 2, など)。

例 1:

入力: 
n = 10 
["call","call","call"]
出力: [10,11,12]
説明: 
counter() = 10 // 最初に counter() が呼び出されたとき、n を返します。
counter() = 11 // 前回より 1 多い値を返します。
counter() = 12 // 前回より 1 多い値を返します。
例 2:

入力: 
n = -2
["call","call","call","call","call"]
出力: [-2,-1,0,1,2]
説明: counter() は最初に -2 を返し、その後の呼び出しごとに値が増加します。
 
制約:

-1000 <= n <= 1000
0 <= calls.length <= 1000
calls[i] === "call"
//デフォルトコードテンプレート
/**
 * @param {number} n
 * @return {Function} counter
 */
var createCounter = function(n) {
    
    return function() {
        
    };
};

/** 
 * const counter = createCounter(10)
 * counter() // 10
 * counter() // 11
 * counter() // 12
 */
ここまで来れば、皆さんはクロージャの基本的な概念を把握していることでしょう。
当該問題の解法や回答については、コメント欄にご意見をお寄せください!
( •̀ ω •́ )y
11
14
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
14