クロージャは、JavaScriptでは非常に重要な概念の一つですが、初心者にとっては抽象的で難解に感じることがあります。特に、ECMA規格での定義は実務経験がないと理解しにくいでしょう。そこで、本記事ではクロージャの概念について長々と説明するのではなく、具体的なコードを通して短時間で理解できるようにします。
1. これがクロージャだ
新しい技術に触れる際に、まず最初にやるべきことは、そのデモコードを探すことです。私たちにとって、コードを読むことは自然言語よりも物事の本質を理解しやすいです。実際、クロージャは至るところで見られます。たとえば、jQuery や Zepto のコアコードは大きなクロージャに包まれています。では、まず最も簡単で基本的なクロージャを紹介します。これにより、クロージャの概念を頭の中にイメージしやすくなるでしょう。
function A(name){
function B(){
console.log(name);
}
return B;
}
var C = A("田所浩二");
C();//田所浩二
これは最も簡単なクロージャです。
これで基本的な理解ができたところで、通常の関数と何が異なるのかを簡単に分析してみましょう。上記のコードを自然言語に翻訳すると、以下のようになります。
- 通常の関数Aを定義する、引数はname
- A の中で通常の関数 B を定義する、関数Bでは外部変数nameを参照する
- A の中で B を返す
- A を実行し、その結果を変数 C に代入する
- 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
コード説明
-
var C = A();
と呼び出すと、A
が実行され、count
変数と内部関数B
が作成されます。A
はB
を返すので、C
変数は実際にはB
の参照を持ちます。 -
関数
B
はA
内のcount
変数にアクセスできます。なぜなら、B
はクロージャであり、クロージャはその作成時のコンテキスト(ローカル変数など)を保持するからです。 -
C()
を呼び出すと、実際にはB
関数が呼び出されます。C()
を呼ぶたびに、B
はcount
の値を増加させ、その値をコンソールに表示します。B
はまだcount
変数を参照しているので、count
の値は増え続けます。 -
A
の実行コンテキストはB
が作成された時点で終了しますが、B
がそのローカル変数(count
など)を参照している限り、A
のローカル変数は回収されません。 -
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
*/