6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptAdvent Calendar 2024

Day 18

JavaScriptで領域を閉じ込める術(初めて使うJavaScriptのクロージャー)

Last updated at Posted at 2024-12-18

はじめに

JavaScriptのクロージャーとは、簡単に言えば「関数の中から、外部スコープの変数(関数の中では宣言していない、関数の外にある変数)を記憶し、参照できる仕組み」です。

「外部スコープの変数を記憶し、参照できる」というのは、「関数の実行が終了しても、その変数を初期化しない状態で使い続けることができる」という意味です。

この仕組みを利用すると、データを安全にカプセル化したり、再利用しやすい関数(ユーティリティ関数)を作成したり、状態管理や初期化の制御を簡単に行うことができます。

通常の関数とクロージャーを利用した関数の違い

通常、関数が実行されると、その中で宣言された変数は実行終了後にメモリから解放されるので、再び関数が実行される時には初期化されています。

一方、クロージャーを利用すると、関数の実行が終わった後も、変数を記憶しておき、関数を再実行する時に利用することが可能になります。

通常の関数
function normalFunction() {
    var count = 0; // 関数が呼び出されるたびに初期化
    
    count++;
    console.log(count);
}

normalFunction(); // 1
normalFunction(); // 1

normalFunctionでは、関数が呼び出されるたびに変数countが初期化され、毎回「1」が表示されます。関数の中で宣言された変数は、実行終了後にメモリから解放されるので、次に実行するときには初期化されてしまうためです。

一方、次のように書けば、クロージャーの仕組みを利用することが出来て変数が記憶されます。

クロージャーを利用した関数
function createCounter() {
    var count = 0; // 外部スコープの変数
    
    return function () { // 内部関数(無名関数)
        count++;
        console.log(count); // 外部スコープの変数を参照
    };
}

var counter = createCounter();
counter(); // 1
counter(); // 2

ここでは、関数 createCounterがreturnで返す内部関数(無名関数)は、外部変数countを「記憶」しています。そのため、関数の実行が終わってもcountの値が保持され、counterを呼び出すたびにその値を加算することができます。

結局、クロージャーを使うと何ができるの?

クロージャーの仕組みを利用することで、次のようなことができます。

1. データのカプセル化

プログラムの他の部分(外側のコード)からは直接触れさせたくないデータを隠しつつ、そのデータを操作するための必要な機能だけを作れる。

2. 柔軟なユーティリティ関数の作成

同じロジックを複数パターンで簡単に使い回すことができる。

3. 一度だけの初期化や状態の保持

処理が一度しか行われないよう制御できる。

クロージャーを使うために知っておくべきこと

クロージャーを理解するために、いくつかの重要な基礎知識を押さえておきましょう。

1. スコープとは?

スコープとは「変数を参照できる範囲や領域」のことです。通常、この領域を超えると変数は参照できなくなりますが、クロージャーを利用することでスコープをまたいだ変数の参照が可能になります。

スコープには、主に次の2種類があります

ローカルスコープ

関数の中で宣言された変数。関数の外からはアクセスできません。

function example() {
    var localVariable = 10; // 関数の中で宣言された変数
    console.log(localVariable);
}

console.log(localVariable); // Error: localVariable is not defined

グローバルスコープ

関数の外で宣言された変数。どこからでもアクセス可能です。

var globalVariable = 10; // 関数の外で宣言された変数

function example() {
    console.log(globalVariable); // 10
}

console.log(globalVariable); // 10

2. JavaScriptの関数は「第一級オブジェクト」

JavaScriptでは関数も「第一級オブジェクト」として扱われます。これは、関数が他のデータ型と同じように「値」として利用できることを意味します。そのため、関数を変数に代入したり、関数の引数や戻り値として渡すことが可能です。この特性がクロージャーの基礎となります。

具体的には「第一級オブジェクト」である関数は次のような特性を持っています。

  1. 変数に代入できる
  2. 関数の引数として渡せる
  3. 関数の戻り値として返せる

これらの特性により、クロージャーという仕組みが成り立ちます。例えば、関数を別の関数に引数として渡したり、関数の返り値として使うことで、特定の「領域」内で変数や状態を保持し、必要に応じてそれを呼び出すような操作が可能になります。

このように、関数を変数に代入したり、引数や戻り値として渡すことができるという特性が、クロージャーを実現する基盤となります。

function greet(name) {
    return function () {
        console.log(`Hello, ${name}!`);
    };
}

var sayHelloToTaro = greet('Taro');
sayHelloToTaro(); // "Hello, Taro!"

3. 実行コンテキストとスコープチェーン

クロージャーを理解するには、JavaScriptの実行コンテキスト(どこでコードが実行されているか)も知っておきましょう。

関数は自分が「定義された場所」を基準にスコープを記憶します。
この特性により、関数が外部スコープに存在する変数を覚えていて、どこで実行されてもその変数にアクセスできる仕組みが実現します。

function outer() { // 関数outer は 関数inner にとって外部スコープ

    var outerVariable = "I am outer!"; // inner にとって外部スコープにある変数
    
    function inner() {
        console.log(outerVariable); // "I am outer!"
    }
    return inner;
}

var myFunction = outer();
myFunction(); // 外部スコープの変数にアクセス

// 関数inner にとって「外側のスコープ」である 関数outer のスコープにある 変数outerVariable を指

この例では、関数inner は 変数outerVariable にアクセスしています。通常、関数outer の実行が終了した後、そのスコープ内の変数(outerVariable)は破棄されるはずですが、関数inner がそのスコープを「覚えている」ため、変数が閉じ込められて残ります。

コードの流れを順を追って説明すると

  1. 関数outerの実行

    • 関数outer が呼び出されると、outerVariable というローカル変数が作成され、値 "I am outer!" が割り当てられます。
    • 関数inner(function inner() { ... })が定義され、関数inner の参照が返されます(return inner;)。
  2. myFunction に 関数inner の参照が保存される

    • var myFunction = outer(); の実行結果として、関数outer の中で定義された 関数inner の参照が myFunction に代入されます。
  3. myFunction() の実行

    • myFunction() を呼び出すと、実際には関数inner が実行されます。
      重要なのは、この時点で 関数outer の実行は既に終了しているにも関わらず、関数inner が outerVariable にアクセスできている点です。

クロージャーの使用例

以下では、クロージャーの使用例を3つのパターンに分けて紹介します。

1. カプセル化

「カウンター」をカプセル化の例として、クロージャーを利用する前と利用した後とで比較します。

クロージャーを利用しない場合
// カウンターを管理する単純なオブジェクト
let counter = {
  value: 0,
  increment: function() {
    this.value++;
  },
  getValue: function() {
    return this.value;
  }
};

// カウンターの操作
counter.increment();
console.log(counter.getValue()); // 1
  • counterオブジェクトがカウンターの状態(value)を管理しています。
  • ただし、counter.value は外部から直接アクセス・変更が可能であり、counter.value = -10 のように意図しない操作ができてしまいます。
クロージャーを利用する場合
function createCounter() {
  // 外部から直接アクセスできないプライベート変数
  let value = 0;

  return {
    increment: function() {
      value++;
    },
    getValue: function() {
      return value;
    }
  };
}

// クロージャーを使ってカウンターを作成
const counter = createCounter();
counter.increment();
console.log(counter.getValue()); // 1
  • value は createCounter 関数の内部に定義されており、外部から直接アクセスすることはできません。
  • increment や getValue を通じてのみ操作が可能です。
  • ポイント:クロージャーを利用することで、データの隠蔽(カプセル化)が実現され、意図しない変更を防ぐことができます。

ポイント

  1. データを外部から直接アクセスできない位置に移動する
    value を let value = 0; のように関数の中に隠し、外部スコープからアクセスできないようにします。
  2. 必要な操作をメソッドとして公開する
    クロージャーを活用して、increment と getValue のようなメソッドを通じてデータを操作します。
  3. カウンターの作成時に初期化を行う
    createCounter を呼び出すたびに独立したカウンターが生成されます。

どちらを使うべき?
外部から直接データを操作させたくない場合や、意図しない変更を防ぎたい場合は、クロージャーによるカプセル化が適しています。
例えば、セキュリティが重要なデータや、特定のモジュール内で管理すべき状態で便利に使えます。

2. ユーティリティ関数

商品の価格に基づいて割引額を計算するユーティリティ関数を作成します。クロージャーを使用することで、割引率を固定した関数を生成し、繰り返し利用できるようにします。

クロージャーを利用しない場合
function calculateDiscount(price, discountRate) {
  return price - price * discountRate;
}

console.log(calculateDiscount(100, 0.1)); // 90
console.log(calculateDiscount(200, 0.1)); // 180
  • 関数 calculateDiscount は、price(価格)と discountRate(割引率)を引数として受け取り、割引後の価格を返します。
  • 割引率が同じ場合でも毎回引数として discountRate を渡す必要があり、コードが少し煩雑です。
クロージャーを利用する場合
function createDiscountCalculator(discountRate) {
  return function(price) {
    return price - price * discountRate;
  };
}

const tenPercentDiscount = createDiscountCalculator(0.1);
console.log(tenPercentDiscount(100)); // 90
console.log(tenPercentDiscount(200)); // 180

1. 関数 createDiscountCalculator は、 discountRate を引数として受け取り、その割引率を固定した新しい関数を返します。

  • 例えば、const tenPercentDiscount = createDiscountCalculator(0.1); の場合、引数 discountRate に 0.1 を固定した関数が生成され、tenPercentDiscount に代入されます。
  • これにより、割引率を都度指定する手間が省け、特定の割引率を利用した処理を簡単に呼び出せるようになります。
    tenPercentDiscount(100) を実行すると、100 が内部関数の price 引数に渡されます。

2. クロージャーが関数 createDiscountCalculator 呼び出し時に固定された discountRate の値 0.1 を記憶しているため、計算は次のように行われます:

100 - 100 * 0.1; // 90

3. クロージャーの仕組みとの関連
クロージャーによって、関数 createDiscountCalculator 内のスコープで定義された discountRate は、返される内部関数からアクセス可能な状態で保持されます。
この仕組みにより、tenPercentDiscount のような特定の割引率に基づく専用関数を簡単に作成できます。

ポイント

  • 割引率の固定: クロージャーを使うことで、割引率を事前に固定したカスタマイズ可能な関数を生成できます。
  • 引数の柔軟性: 生成された関数は価格のみを引数に取り、割引率を再指定する必要がありません。
  • 可読性と再利用性: 割引率ごとに関数を用意することで、異なる割引率を使い分けるコードが簡潔になります。

3. 初期化処理

一度だけ特定の設定やデータの初期化を行い、その結果を後の処理で再利用するケース。

クロージャーを利用しない場合
// 設定値を初期化する
let isInitialized = false;
let config;

function initializeConfig() {
  if (!isInitialized) {
    config = { appName: "MyApp", version: "1.0.0" }; // 初期化処理
    isInitialized = true;
    console.log("Config initialized.");
  } else {
    console.log("Config already initialized.");
  }
}

// 設定値を使用する
initializeConfig();
console.log(config); // { appName: "MyApp", version: "1.0.0" }
initializeConfig(); // 再初期化は行われない
  • 初期化状態を示す isInitialized フラグを用いて、二度目以降の初期化を防いでいます。
  • config はグローバル変数として定義されており、外部から直接アクセス・変更が可能です。
クロージャーを利用する場合
function createConfigInitializer() {
  let config;
  let isInitialized = false;

  return function() {
    if (!isInitialized) {
      config = { appName: "MyApp", version: "1.0.0" }; // 初期化処理
      isInitialized = true;
      console.log("Config initialized.");
    } else {
      console.log("Config already initialized.");
    }
    return config;
  };
}

// クロージャーを使って初期化関数を作成
const initializeConfig = createConfigInitializer();
console.log(initializeConfig()); // { appName: "MyApp", version: "1.0.0" }
console.log(initializeConfig()); // 再初期化は行われない
  • クロージャーを使うことで、config と isInitialized は関数内のローカル変数として管理されます。
  • 外部からは直接アクセスできず、初期化の制御が安全に行われます。
  • 初期化処理は一度だけ実行され、その後の呼び出しでは既存の config を返すようになります。

ポイント

  1. 初期化用の状態をカプセル化する
    config や isInitialized をローカル変数として関数内に閉じ込め、外部スコープに露出しないようにします。

  2. 初期化処理をカプセル化した関数として提供する
    初期化処理を一度だけ実行するための処理をクロージャーで囲みます。

  3. 再利用性を高める
    必要な場合に同様の処理で異なる初期化処理を作成できる柔軟性が得られます。

どちらを使うべき?
初期化処理が複雑で、一度だけ確実に実行したい場合にはクロージャーが有効です。
外部からの状態変更を防ぎたい場合に適しています。

あとがき

クロージャーは、呪術廻戦における「簡易領域」のように感じられました。軽量で特定の目的に迅速に展開できるものの、クラスやプロトタイプのような高度な設計には適していません。一方、「本格的な領域展開」はクラスやプロトタイプを使うと、より複雑で高度な設計ができます。クロージャーは簡易的なカプセル化に、クラスやプロトタイプはより大規模で詳細なカプセル化に向いています。使い分けが大事ですね。

6
4
2

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
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?