0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

クロージャとは何じゃ?

Posted at

はじめに

クロージャとは?

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

クロージャは関数とその関数が作られた環境という 2 つのものの組み合わせです。この環境は、クロージャが作られた時点でスコープ内部にあったあらゆるローカル変数によって構成されています。

クロージャ - JavaScript | MDN

:thinking::question:

クロージャの前に

クロージャを理解するには、まずレキシカルスコープという概念を知る必要がある。クロージャは関数内で変数の参照を保持できる特徴があり、その挙動がレキシカルスコープに基づいて決まる。

レキシカルスコープってどんなスコープ?

レキシカルスコープとは?

関数が定義された場所でアクセスできる変数が決まるスコープのことで、
自分が所属する外側のスコープがレキシカルスコープになる。

レキシカルスコープの特徴

:writing_hand: 関数が定義された場所で、どの変数にアクセスできるか決定

:writing_hand: 内側の関数が所属するスコープの外側のスコープがレキシカルスコープになる

→実行中のコードから見た外側のスコープ

レキシカルスコープの例 1
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 が見つかる → '内側の関数の変数'

レキシカルスコープの例 2
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 の値を変更することができてしまう → クロージャではない

状態のカプセル化がされておらず、予期しない動作を引き起こす可能性がある。

クロージャにすると、外部からの変更を防ぎ、安全に状態を管理することができる

クロージャを使う時はガベージコレクションを意識する

ガベージコレクション
どこからも参照されなくなった変数を不要なデータと判断して自動的にメモリ上から削除する仕組みのこと

クロージャの特徴に変数のスコープの保持がある。関数が生成された時点の環境を保持することで、外部関数の変数を長期間メモリに保持する可能性がある。

「関数の中で作成したデータは、その関数の実行が終了したら解放される」というわけではない。関数の中で作成したデータは、「関数の実行が終了した際に解放される場合」と「関数の実行が終了しても解放されない場合」がある。

:arrow_right: クロージャによってメモリリークが発生し、ガベージコレクションに影響する。

メモリリーク
メモリを確保した後、使い終わったメモリを解放せずに放置すると、不要なメモリが残り続ける現象。システム全体のパフォーマンスの低下の原因となる。

例)関数の実行が終了した際に解放される
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 | MDN

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

クロージャは関数とその関数が作られた環境という 2 つのものの組み合わせです。この環境は、クロージャが作られた時点でスコープ内部にあったあらゆるローカル変数によって構成されています。

【JavaScript】クロージャについて解説

クロージャとは、関数内で使用されている変数がレキシカルスコープの変数の値を保持し続けている状態

【JavaScript】スコープとクロージャ

クロージャとは、レキシカルスコープの変数を関数が使用している状態のこと

変数スコープ、クロージャ

クロージャ(closure) は外部変数を記憶し、それらにアクセスできる関数

JavaScriptの即時関数とクロージャについて現役エンジニアが解説【初心者向け】 | TechAcademyマガジン

クロージャとは内部関数が戻り値になる構造のことです。

JavaScript - クロージャと引数の関係を理解しよう【完全ガイド】|Kevi Log (ケビろぐ!)

クロージャとは、関数が他の関数内で定義され、その外部関数の変数にアクセスできる機能です。

【JavaScript】クロージャー(Closure)について

クロージャとは「レキシカルスコープにある変数や引数への参照を保持し続ける」という関数が持つ性質のことです。

関数とスコープ · JavaScript Primer #jsprimer

クロージャーとは「外側のスコープにある変数への参照を保持できる」という関数が持つ性質のことです。

【JavaScriptの基礎】レキシカルスコープとクロージャを理解する | WEMO

クロージャは「関数」と「その関数が作られた環境」という 2 つのものが一体となった特殊なオブジェクト

0
0
4

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?