はじめに
クロージャとは?
クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。言い換えれば、クロージャは内側の関数から外側の関数スコープへのアクセスを提供します。JavaScript では、関数が作成されるたびにクロージャが作成されます。
クロージャは関数とその関数が作られた環境という 2 つのものの組み合わせです。この環境は、クロージャが作られた時点でスコープ内部にあったあらゆるローカル変数によって構成されています。
クロージャの前に
クロージャを理解するには、まずレキシカルスコープという概念を知る必要がある。クロージャは関数内で変数の参照を保持できる特徴があり、その挙動がレキシカルスコープに基づいて決まる。
レキシカルスコープってどんなスコープ?
レキシカルスコープとは?
関数が定義された場所でアクセスできる変数が決まるスコープのことで、
自分が所属する外側のスコープがレキシカルスコープになる。
レキシカルスコープの特徴
関数が定義された場所で、どの変数にアクセスできるか決定
内側の関数が所属するスコープの外側のスコープがレキシカルスコープになる
→実行中のコードから見た外側のスコープ
let global = 'グローバル変数';
function outerFunc() {
let outer = '外側の関数の変数';
function innerFunc() {
let inner = '内側の関数の変数';
console.log(global); // 'グローバル変数'
console.log(outer); // '外側の関数の変数'
console.log(inner); // '内側の関数の変数'
}
innerFunc();
}
outerFunc();
- レキシカルスコープは、関数が定義された場所で、どの変数にアクセスできるか決まる
- 自分のスコープ内にない場合、外側のスコープ(親スコープ)へ探しに行く
console.log(global)
:
関数 innerFunc 内で global が見つからない → その外側の関数 outerFunc 内にもない → さらに外側のグローバルスコープを探す → 'グローバル変数'
console.log(outer)
:
関数 innerFunc 内で outer が見つからない → その外側のスコープ(outerFunc)にアクセス → '外側の関数の変数'
console.log(inner)
:
関数 innerFunc 内で inner が見つかる → '内側の関数の変数'
let x = 10;
function A(){
console.log(x); //この時の静的なスコープは x=10
}
function B(){
let x = 1000; //ここでもxが定義されている
A(); //この時のxは 10? 1000?
}
A(); // 10
B(); // ?
レキシカルスコープは、関数が定義された場所で参照する変数が決まる。
→ 関数 B の内部で関数 A が呼ばれる時ではなく、関数が定義された時に決まっている。
関数 A の内部で x にアクセスすると、所属するスコープの外側のグローバル変数 x にアクセスする事になる。
B(); // 10
クロージャの基本形
関数とその中にある変数、内部関数で構成され、内部関数が関数の戻り値となる。
関数 {
変数
return 内部関数
}
クロージャの特徴
変数のスコープの保持
「変数のスコープの保持」とは?
外部関数のスコープ内で定義された変数へのアクセスを可能に。
→ 外部関数が実行を終了しても、クロージャ内で定義された変数は引き続き利用可能に。
→ これにより、変数の値や状態を保持し、後続の関数呼び出しで再利用が可能になり、状態管理しやすくなる。
function createCounter() { // 外部関数
let count = 0; // 外部関数のスコープ内で定義された変数 内部関数からアクセス可能
return function() { // 内部関数(クロージャ)
count++; // スコープを保持し、countの値も保持
return count;
};
}
const counter = createCounter(); // 外部関数を実行、戻り値であるクロージャをcounterに格納
counter(); // クロージャを呼び出す
プライベート変数としての機能
「プライベート変数としての機能」とは?
クロージャ内で定義された変数は、外部からアクセスできない。
→ 変数の不要な変更/誤用を防止し、安全で整合性のあるコードの作成に役立つ。
function createCounter() { // 外部関数
let count = 0; // 外部関数のスコープ内で定義された変数 内部関数からアクセス可能
return function() { // 内部関数(クロージャ)
count++; // スコープを保持し、countの値も保持
return count;
};
}
const counter = createCounter(); // 外部関数を実行、戻り値であるクロージャをcounterに格納
counter(); // クロージャを呼び出す
// console.log(count) 外部からアクセスできない
// count = 10; としても代入不可、状態を保持
関数の再利用
「関数の再利用」とは?
状態を持つ関数を生成することができる
→特定のデータを閉じ込めておくことで、同じ関数を異なる状態で再利用できる。
- 「Don't Repeat Yourself」(繰り返さない)というDRY原則に従い、同じコードを繰り返さないようにすることができ、これによりエラーのリスクが減る。
function createCounter() { // 外部関数
let count = 0; // 外部関数のスコープ内で定義された変数 内部関数からアクセス可能
return function() { // 内部関数(クロージャ)
count++; // スコープを保持し、countの値も保持
return count;
};
}
const counter1 = createCounter(); // 最初のカウンター
const counter2 = createCounter(); // 別のカウンター 再利用
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // ?
console.log(counter2()); // ?
counter2の結果は、
console.log(counter2()); // 1
console.log(counter2()); // 2
それぞれが独立し別々の変数を参照している。
→ 各呼び出しで新しいスコープが作成され、各々は自分自身の状態を保持する。
外側の関数を変数に代入する意味
function outerFn() {
const outerVariable = 'outer scope';
function innerFn() {
console.log(outerVariable); //outerFn()によりinnerFnを返す
}
return innerFn;
}
const closure = outerFn(); // 代入して外側の関数のスコープを保持する
closure(); // 内側の関数を後で呼び出せる
const closure = outerFn(); ← 外側の関数を変数に代入する意味
通常は、外側の関数 outerFn の中で定義された変数 outerVariable は、そのスコープ内でのみ有効
1)outerFn()によりinnerFnを返す
function outerFn() {
const outerVariable = 'outer scope';
function innerFn() {
console.log(outerVariable);
}
return innerFn; // innerFn を返す
}
2)変数 closureに代入して、 外側のスコープを保持
const closure = outerFn();
変数に代入する事で、innerFnを保持 + outerFnのスコープも保持
3)closure()を実行する
closure();
innerFnを呼び出す事になり、クロージャーによって保持された参照により外部からでもouterVariableにアクセスできる。
outerFn();
もし代入を行わなかった場合、
- 内側の関数 innerFnへの参照を失う(保持できない)
- その関数を"後で"呼び出せない
outerFn(); // クロージャが生成... しかし、どこからも参照されない
「クロージャが生成される」とは、
- 内側の関数 innerFn が定義され、外側の関数 outerFn のスコープにある outerVariable を参照できる状態に
- このレキシカルスコープによって変数への参照がいったん保持される
しかし、外部からアクセスできない状態では、そのクロージャは無意味になる
↓ つまり、
クロージャを有効活用にするには、どこかに参照を持たせることが必要
レキシカルスコープは存在するが、クロージャではない
外側の関数(外側の関数によるスコープ)がない状態を例にする。
let num = 0; // グローバル変数
function increment() { // 外側の関数が無い、numが外部からアクセス可能な状態
num = num + 1;
console.log(num);
}
increment(); // 1
increment(); // 2
num = 5; // numの値を外部から変更できてしまう
increment(); // 6(ここでは 変更された事によってnumが 5 からスタート)
レキシカルスコープは「関数が定義された場所のスコープに基づいて変数にアクセスする」
-
increment 関数がグローバルスコープで定義されており、グローバルスコープ内の num にアクセスできる → レキシカルスコープ
-
グローバルスコープにある num が外部からアクセス可能なため、後から関数を呼んでも、他の場所で num の値を変更することができてしまう → クロージャではない
状態のカプセル化がされておらず、予期しない動作を引き起こす可能性がある。
→ クロージャにすると、外部からの変更を防ぎ、安全に状態を管理することができる。
クロージャを使う時はガベージコレクションを意識する
ガベージコレクション
どこからも参照されなくなった変数を不要なデータと判断して自動的にメモリ上から削除する仕組みのこと
クロージャの特徴に変数のスコープの保持がある。関数が生成された時点の環境を保持することで、外部関数の変数を長期間メモリに保持する可能性がある。
「関数の中で作成したデータは、その関数の実行が終了したら解放される」というわけではない。関数の中で作成したデータは、「関数の実行が終了した際に解放される場合」と「関数の実行が終了しても解放されない場合」がある。
クロージャによってメモリリークが発生し、ガベージコレクションに影響する。
メモリリーク
メモリを確保した後、使い終わったメモリを解放せずに放置すると、不要なメモリが残り続ける現象。システム全体のパフォーマンスの低下の原因となる。
function printX() {
const x = "Hoge";
console.log(x); // => "Hoge"
}
printX();
// "Hoge"を参照するものはなくなる -> 解放される
function createArray() {
const array = [1, 2, 3];
return function() {
console.log(array); // 外部変数 `array` を参照
};
}
const closure = createArray(); // 内部関数が返され、クロージャが作成される
closure(); // [1, 2, 3] という値を参照している -> 解放されない
- createArray()の返り値として closure に代入すると、closure がその array を保持し
createArray() の実行が終了しても解放されない
closure の参照を解放するには、
closure = null
function createArray() {
const array = [1, 2, 3];
return function() {
console.log(array); // 外部変数 `array` を参照
};
}
const closure = createArray();
closure();
closure = null // 参照が解放される
closure = null とすることで、closure が参照していたクロージャも解放され、array もガーベジコレクションの対象となる。
クロージャでグローバル変数を減らす
クロージャはグローバル変数を減らす有効な手段といえる。
グローバル変数:
アプリケーション全体でアクセス可能なため、予期しない副作用や名前の衝突が発生しやすくなる
let counter = 0; // グローバル変数
function increment() {
counter++;
console.log(counter);
}
function decrement() {
counter--;
console.log(counter);
}
increment(); // 1
increment(); // 2
decrement(); // 1
クロージャを使うと、
function createCounter() {
let count = 0; // プライベート変数(グローバル変数を使っていない)
return {
increment: function() {
count++;
console.log(count);
},
decrement: function() {
count--;
console.log(count);
}
};
}
const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
この様に、クロージャを使用すると、データをプライベートに保ちながら、必要な部分にだけアクセスできるようにすることができる。(他のソースコードへの影響がない)
クロージャは、グローバル変数の使用を減らすために非常に有効な手段
クロージャと高階関数 結果は同じだが...
高階関数が必ずクロージャを生成するわけではないが、高階関数の一部の使い方にはクロージャの性質が関係していることがあり、クロージャを作るケースがある。
まず、普通の関数と高階関数の違いは、
- 普通の関数 → 単に引数を受け取って結果を返す
- 高階関数 → 関数を引数として受け取ったり、返り値として関数を返したりする
function add(a, b) {
return a + b;
}
console.log(add(1, 2)); // 3
function add() {
return function(x) { // 高階関数の特徴
return x + 1;
};
}
const increment = add(); // add を呼び出すと、関数が返される
console.log(increment(2)); // 3
この例では、"外部の変数を参照しない"関数が返されるため、クロージャは発生しない。
クロージャが発生する条件:
返される関数が外部スコープの変数を参照している必要がある
これを、高階関数でクロージャにするには、
function add(num) {
return function(x) { // クロージャ
return x + num; // num(外部スコープの変数)にアクセス
};
}
const increment = add(1);
console.log(increment(2)); // 3
返された関数は、外部の変数 num を「記憶」しており、num にアクセスできるため、クロージャといえる。
ここで、高階関数でクロージャだが、外側の関数が引数を持つか持たないかを比べてみる。
function multiplier() {
let num = 2; // num を内部で固定
return function(x) {
return x * num;
};
}
const double = multiplier();
console.log(double(5)); // 10
numは内部で 2 に固定されおり、返された関数は、常に2倍の計算しかできない。
高階関数でクロージャだが、
num を固定しているため、高階関数の"柔軟性"や"再利用性"が活かされていない
num の値を動的に設定できるようにしたい場合、外側の関数に引数を与えると、numを外部から指定できる様になる。
function multiplier(num) {
return function(x) {
return x * num;
};
}
const double = multiplier(2); // 2で掛け算する関数を作る
console.log(double(5)); // 10
const triple = multiplier(3); // 3で掛け算する関数を作る
console.log(triple(5)); // 15
外側の関数に引数として受け取るようにすることで、
- 動的な関数として、高階関数の本来の特徴である再利用性が高まる
- 引数に意味を持たせる事で、可読性も高まる
参考
クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。言い換えれば、クロージャは内側の関数から外側の関数スコープへのアクセスを提供します。JavaScript では、関数が作成されるたびにクロージャが作成されます。
クロージャは関数とその関数が作られた環境という 2 つのものの組み合わせです。この環境は、クロージャが作られた時点でスコープ内部にあったあらゆるローカル変数によって構成されています。
クロージャとは、関数内で使用されている変数がレキシカルスコープの変数の値を保持し続けている状態
クロージャとは、レキシカルスコープの変数を関数が使用している状態のこと
クロージャ(closure) は外部変数を記憶し、それらにアクセスできる関数
JavaScriptの即時関数とクロージャについて現役エンジニアが解説【初心者向け】 | TechAcademyマガジン
クロージャとは内部関数が戻り値になる構造のことです。
JavaScript - クロージャと引数の関係を理解しよう【完全ガイド】|Kevi Log (ケビろぐ!)
クロージャとは、関数が他の関数内で定義され、その外部関数の変数にアクセスできる機能です。
【JavaScript】クロージャー(Closure)について
クロージャとは「レキシカルスコープにある変数や引数への参照を保持し続ける」という関数が持つ性質のことです。
関数とスコープ · JavaScript Primer #jsprimer
クロージャーとは「外側のスコープにある変数への参照を保持できる」という関数が持つ性質のことです。
【JavaScriptの基礎】レキシカルスコープとクロージャを理解する | WEMO
クロージャは「関数」と「その関数が作られた環境」という 2 つのものが一体となった特殊なオブジェクト