クロージャについて学習しました。この記事は、独習JavaScriptとJavaScript Primerで勉強した内容を記述しています。
まず結論
クロージャとは「レキシカルスコープにある変数や引数への参照を保持し続ける」という関数が持つ性質のことです。
以下は関数countUpが実行されるごとに一ずつ大きくなるクロージャの例です↓
function countFactory() {
let count = 0;
return () => {
count += 1;
return count;
};
}
const countUp = countFactory();
console.log(countUp()); // 1
console.log(countUp()); // 2
以降より説明していきます。
前提知識
以下はクロージャに関わる2つの仕組みです↓
- レキシカルスコープ
- ガベージコレクション
レキシカルスコープ(Lexical Scope)
まずJavaScriptにはスコープと呼ばれる仕組みがあります。これは、変数や関数、関数の引数などを現在実行中のコードから見て参照できる範囲のことです。
スコープの性質として記述した時点でどの識別子がどの変数を参照するか決定されます。どのスコープに書かれた変数を参照するのか決定しています。
レキシカルスコープはこの一種で、記述する場所によって参照できる変数が異なるスコープのことです。具体的には、実行中のコードが属するスコープ(自スコープ)の外側のスコープを指します。
以下はスコープの例です↓
//スクリプトコープ
const number = 1;
function Fn() {
//関数Fn内のスコープ(関数スコープ)から見て、変数numberはレキシカルスコープである。
//console.logに書かれた識別子"number"は、常にレキシカルスコープである変数numberの値を参照する。
console.log(number);
}
function run() {
/*関数run内で新たに変数numberが宣言されていますが、
記述された時点でどの変数を参照するか決定しているので常に1を参照します。*/
const number = 10;
Fn();
}
run();// 常に1が出力
//【識別子numberの参照過程】
//1:Fnの関数スコープに変数numberが定義されていない
//2:ひとつ外側のスコープ(スクリプトスコープ)を確認する
//3:スクリプトスコープにconst number = 1;が定義されているので、識別子xはこの変数を参照する
詳しくは以下のサイトでご確認ください↓
ガベージコレクション
ガベージコレクションとは、どこからも参照されなくなった変数を不要なデータと判断して自動的にメモリ上から削除する仕組みのことです。PCのメモリは有限のため、要らなくなったデータを削除する必要があります。
一般的なガベージコレクションが起きる流れを以下のコードで示します↓
let number = 1;
number = 10;
//【ガベージコレクションが起きる流れ】
//1:まず、「1」という数値のデータがメモリ上に作成されます。
//2:次に変数numberが、そのメモリを参照します。
//3;新たに「10」という数値のデータがメモリ上に作成されます。
//4:変数numberの参照先が「1」から「10」に変更されます。
//5:「1」というデータはどこからも参照されなくなったため、ガベージコレクションの対象となり削除されます。
次に関数とガベージコレクションの関係を見ていきます。
関数の中で作成したデータは、「関数の実行が終了した際に削除される場合」と「関数の実行が終了しても削除されない場合」が存在します。
まず、関数の実行が終了した際に削除される場合の例は以下です↓
function number() {
const one = 1;
console.log(one) // 1
}
number();
//関数の実行が終了すると、1というデータはどこからも参照されなくなり、削除されます。
続いて関数の実行が終了しても削除されない場合の例です↓
function createCount() {
const count = 1;
return count;
}
const number = createCount();
// 変数numberが関数createCount内の1という値を参照しているため、削除されない
console.log(number) // 1
詳しくはこちら↓
改めてクロージャとは
これまでで、レキシカルスコープとガベージコレクションについて記述してきました。
クロージャはこれらを組み合わせて、関数内から特定の変数や引数を参照し続けることで関数が状態を持てる仕組みのことを言います。
最初のクロージャの例をもう一度見てみます↓
function countFactory() {
//無名関数から見て関数countFactoryはレキシカルスコープに当たります。
//エンクロージャ
let count = 0;
return () => {
//無名関数内のcountはcountFactoryの変数countの値を参照しています。
//クロージャ
count += 1;
return count;
};
}
//関数countFactoryを実行すると、返り値として無名関数が変数countUpに渡されます。
const countUp = countFactory();
//countUpの実行結果は、countの計算結果です。
console.log(countUp()); // 1
console.log(countUp()); // 2
つまり以下のような参照の関係が変数countUpと変数countにあることが分かります↓
- 変数countUpは、関数countFactoryの返り値である無名関数を参照している。
- 変数countUpは、無名関数を経由して変数countを参照している。
- 変数countUpを実行した後でも、変数countへの参照は保たれる。
この関係を検証ツールを用いてデバックした動画で示してみました↓
よって変数countを参照するものがいるため、変数countはガベージコレクションの対象となりません。そのため、変数countの値は保持され続けて、変数countUpを実行するたびに一ずつ大きくなります。
クロージャを使う場面
関数内部でのみ使用する変数を宣言する
もし、常に一ずつ大きくなる関数を以下のコードのように実装するとうまくいきません↓
function countUp() {
let count = 0;
count += 1;
console.log(count);
}
countUp() // 1
//関数の実行時に毎回0に初期化されるため、1
countUp() // 1
上記のコードは、変数countが実行時に値が0に初期化されて、常に一が出力されます。
以下のように変数countを関数countUpより外側で宣言すると、一ずつ大きくなります↓
let count = 0;
function countUp() {
count += 1;
console.log(count);
}
countUp(); // 1
countUp(); // 2
上記のように記述すると、関数実行のたびに常に一ずつ大きくできます。しかし、変数countはグローバルスコープに存在しているため、変数countがどこかで違う値を参照されてしまうと意図しない挙動になってしまいます↓
let count = 0;
function countUp() {
count += 1;
console.log(count);
}
countUp(); // 1
countUp(); // 2
count = 10;
countUp() // 11
そのため、クロージャを使って変数countの影響範囲を関数内に閉じ込めることで意図しない挙動になるのを極力防げます↓
function countFactory() {
let count = 0;
return () => {
count += 1;
console.log(count);
};
}
const countUp = countFactory();
countUp(); // 1
countUp(); // 2
これ以外にもクロージャの用途や意識する場面があると思います。また気づいたら追加していきます。
Reactを勉強していると「ステートの更新が反映されない」問題に直面し、これもクロージャが関わっているようです↓