クロージャを理解しよう!〜カウンターを作りながら学ぶJS〜

今回の記事では、JavaScriptの少し難しい概念「クロージャ」について詳しく解説していきます。私もそうでしたが、初学者には難しい表現ですので、しっかりと噛み砕いて解説します。

プロ太
プロ太

さあ、OZくん。今日はJavaScriptのクロージャについて勉強していきましょう!
クロージャとは、簡単に言うと「関数と、その関数が作られた時の環境がセットになったもの」です。関数が引数や自分の外側で定義された変数を覚えていられる、とても便利な機能なんですよ。

えっ、なんですかそれ。関数が引数や変数を覚えるってどういうことですか?めっちゃ難しそう…

はい、確かに言葉だけだと分かりにくいですよね。じゃあ、具体的な例を見ながら理解していきましょう。まずはシンプルな挨拶をする関数から見てみましょう。

function makeGreeting(name) {
    const message = "こんにちは、";
    
    function greet() {
        console.log(message + name + "さん!");
    }
    
    return greet;
}

const greetOz = makeGreeting("OZ");
greetOz(); // "こんにちは、OZさん!"と表示される

なんか関数の中に関数があってめっちゃごちゃごちゃしてんな〜。どないなってんのかさっぱりです…

じゃあ、このコードで起きていることを順番に見ていきましょう。

まず、外側のmakeGreeting関数では3つのことが起きています

  1. nameというパラメータを受け取って
  2. messageという定数を定義して
  3. greetという関数を作って返している

そして、ここが、クロージャの面白いところなんですが、クロージャは、関数が作られた時の引数・変数を「覚えていられる」んです。下のコードの例だと、makeGreeting(name)がクロージャで、引数であるnameに代入した値を作成時に覚えておき、その後でもずっと覚えた状態で使うことができます。

// 通常の関数では
function hello() {
    const message = "こんにちは";
    console.log(message);
}
hello(); // "こんにちは"

// クロージャでは
function makeGreeting(name) {
    const message = "こんにちは、";
    
    // この関数は、messageとnameの値を覚えます!
    function greet() {
        console.log(message + name + "さん!");
    }
    
    return greet;
}
// ↓ここが大切!!
const greetOz = makeGreeting("OZ");  // この時の"OZ"という値を覚えている
greetOz(); // "こんにちは、OZさん!"

へぇ!関数が変数や引数の値を”覚えている”んですね!

そうなんです。こんな感じでイメージするとわかりやすいかもしれません!

※以下は、あくまで、クロージャの個人のイメージです。

// greetOzは、こんな感じで値を覚えています
// greetOz = {
//   関数: function greet() { ... },
//   覚えている変数: {
//     message: "こんにちは、",
//     name: "OZ"
//   }
// }

おっ!これ見て、なんかちょっと分かってきましたわ!関数が変数や引数を”覚えている”感じが伝わってきます!だから、クロージャとは、簡単に言うと「関数と、その関数が作られた時の環境がセットになったもの」なんですね。

その通りです!だから、クロージャを使えば、同じ関数でも、違う名前を渡せば違う挨拶ができるんですよ。

const greetOz = makeGreeting("OZ");
const greetPurota = makeGreeting("プロ太");

greetOz();   // "こんにちは、OZさん!"
greetPurota(); // "こんにちは、プロ太さん!"

// greetOzは、こんな感じで値を覚えています
// greetOz = {
//   関数: function greet() { ... },
//   覚えている変数: {
//     message: "こんにちは、",
//     name: "OZ"
//   }
// }

// greetPurotaは、こんな感じで値を覚えています
// greetPurota = {
//   関数: function greet() { ... },
//   覚えている変数: {
//     message: "こんにちは、",
//     name: "プロ太"
//   }
// }

なるほど!これで完璧にクロージャがどんなものか理解できました!
先生!このクロージャは他にもどんなことで使われてるんですか?

いい質問ですね。では、もっと実践的な例として、カウンターを作ってみましょう!

function createCounter() {
    let count = 0;  // プライベートな変数
    
    return function() {
        count += 1;
        return count;
    };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

おおっ!なんか面白そうですね。もしかして、今度のこのコードは、クロージャが、引数ではなくて変数を覚えておくパターンですか?

そのとおりです!これがクロージャの本質です。createCounter関数の中で定義されたcount変数は、返された関数たちが覚えていて、アクセスできるんです。

function createCounter() {
    let count = 0;  // プライベートな変数
    
    return function() {
        count += 1;
        return count;
    };
}

const counter = createCounter();
console.log(counter()); // 1
↑
この時点で、
// counterは、こんな感じで値を覚えています
// counter = {
//   関数: function () { ... },
//   覚えている変数: {
//     count: 1
//   }
// }

console.log(counter()); // 2
↑
同様にこの時点で、
// counterは、こんな感じで値を覚えています
// counter = {
//   関数: function () { ... },
//   覚えている変数: {
//     count: 2
//   }
// }

console.log(counter()); // 3

補足説明:console.log(counter)じゃないの?

上記のコードを見たときに、const counter = createCounter();としてるんだから、 console.log(counter)とすれば、関数が実行されんじゃないの?と思った方はいませんか?実は、私も最初はそうでした。よって、このあたりをもう少し、詳しく解説しておきます。余裕で分かる人はスルーしてくださいね。

function createCounter() {
    let count = 0;
    return function() {
        count += 1;
        return count;
    };
}

// これだけだと何も起きない(戻り値の関数を受け取るだけ)
createCounter();  

// 変数に保存しないと、返された関数を後で使えない
createCounter()(); // これなら1が返される(すぐに実行するなら可能)

// 普通はこう書く(後で何回でも使えるように変数に保存)
const counter = createCounter();  // カウンター関数を作って保存
console.log(counter());  // 1 (ここで初めてカウントアップの処理が実行される)

createCounter()を実行すると、

  1. 新しいcount変数(値は0)が作られる
  2. そのcountを覚えている関数が返される
  3. でもまだカウントアップは実行されていない

つまり、この段階では、まだfunction()がリターンされただけで、実行されていません。

返された関数を実行して初めて、

  1. countが1増える
  2. 増えた値が返される

という流れになります。

つまり:

  • counter = createCounter() → カウンター関数を作って返す関数を変数に格納
  • counter() → 実際に関数を実行してカウントを増やす

という2段階の処理になっているんです!
細かいですが、こういうところまでしっかりと理解しておきたいですよね。

では、ここで、クロージャの実用的な例をもう一つ見てみましょう!

function createPasswordChecker(correctPassword) {
    return function(inputPassword) {
        return correctPassword === inputPassword;
    };
}

const checkMyPassword = createPasswordChecker("secret123");
console.log(checkMyPassword("wrong")); // false
console.log(checkMyPassword("secret123")); // true

なるほど!!パスワードチェックみたいなのにも使えるんやな!!

そうです!クロージャを使うと、データを隠しながら必要な機能だけを提供できるんです。これをカプセル化っていうんですけど…

ああ!!分かります!!オブジェクト指向の授業で習いました!!

おお!!よく覚えていましたね。じゃあ、最後にクロージャを使って、商品の在庫管理システムのプログラムを書いてみますね。

function createInventoryManager(initialStock) {
    let stock = initialStock;
    
    return {
        addStock: function(amount) {
            stock += amount;
            return `在庫が${amount}個追加され、総在庫は${stock}個になりました。`;
        },
        removeStock: function(amount) {
            if (stock >= amount) {
                stock -= amount;
                return `在庫から${amount}個減り、残り${stock}個です。`;
            } else {
                return "在庫が足りません!";
            }
        },
        checkStock: function() {
            return `現在の在庫は${stock}個です。`;
        }
    };
}

const gameConsoleStock = createInventoryManager(5);
console.log(gameConsoleStock.checkStock()); // "現在の在庫は5個です。"
console.log(gameConsoleStock.addStock(3)); // "在庫が3個追加され、総在庫は8個になりました。"
console.log(gameConsoleStock.removeStock(6)); // "在庫から6個減り、残り2個です。"

では、細かく1行毎に見ていきましょう!

//1行目
function createInventoryManager(initialStock) {

最初に「createInventoryManager」という関数を作っていますね。この関数は「在庫管理をするオブジェクト」を返してくれる役割を持っています。

//2行目
let stock = initialStock;

ここでは、変数「stock」を定義して、初期の在庫数として関数の引数で受け取ったinitialStock」を代入しています。

じゃあ、このstockが「今ある在庫数」になるんですね。

その通りです!そして、関数の中で定義した変数だから、外からは直接触れないようになっていて安全です。

//3行目 
return {

この部分では関数が「オブジェクト」を返そうとしています。今回であれば、「在庫を操作するためのメソッドたち」を持ったオブジェクトになりますね。

なるほど!!オブジェクトの中に、在庫を増やしたり確認する関数をまとめるんですね!

//4〜8行目 
addStock: function(amount) {
            stock += amount;
            return `在庫が${amount}個追加され、総在庫は${stock}個になりました。`;
        },

まずは「addStock」というメソッドですね。これを呼び出すと在庫に新しい商品が追加されます。

amountは追加する数ですね?

そうですね。stockamountを足して、結果をメッセージとして返しています。
たとえば、3個追加すると「在庫が3個追加され、総在庫は8個になりました」という感じで出力されますよ。

//9〜15行目 
removeStock: function(amount) {
            if (stock >= amount) {
                stock -= amount;
                return `在庫から${amount}個減り、残り${stock}個です。`;
            } else {
                return "在庫が足りません!";
            }
        },

次に「removeStock」というメソッドを見てみましょう。これは在庫を減らすためのメソッドになります。

在庫が減るときも、減らした数と残りの在庫数がメッセージで返されるんですね。

そうです。でも、もし減らしたい数より在庫が足りなかった場合は「在庫が足りません!」と返すようになっています。

//16〜18行目 
checkStock: function() {
            return `現在の在庫は${stock}個です。`;
        }

最後は「checkStock」というメソッド。これは今の在庫数を確認するためのものですね。stockの値をそのままメッセージで返してくれますよ。

//20〜24行目 
const gameConsoleStock = createInventoryManager(5);
console.log(gameConsoleStock.checkStock()); // "現在の在庫は5個です。"
console.log(gameConsoleStock.addStock(3)); // "在庫が3個追加され、総在庫は8個になりました。"
console.log(gameConsoleStock.removeStock(6)); // "在庫から6個減り、残り2個です。"

const gameConsoleStock = createInventoryManager(5);で、
この関数を使って新しい在庫管理を始めています。今回は「ゲーム機の在庫」を管理しています。初期在庫として5個を指定しています。

これで「ゲーム機」の在庫を管理するインスタンスができたんですね。

その後、checkStockメソッドを使って、今の在庫数を確認しているね。結果は「現在の在庫は5個です」となります。
そしてさらに、addStockで3個追加していますね。このときのメッセージは「在庫が3個追加され、総在庫は8個になりました」となります。
そして、最後に、removeStockで6個減らしています。結果として「在庫から6個減り、残り2個です」と表示されます。

なるほど!!残りの数もちゃんと計算されてますね!これ、めっちゃ実用的ですね!在庫数をプライベートに保持しながら、必要な操作だけできるようになってる

そのとおりです。クロージャを使うと、このように変数を安全に管理しながら、必要な機能だけを提供できるです。

まとめ

クロージャの重要なポイント:

  1. 関数が、引数や自分の外側で定義された変数を覚えていられる
  2. プライベートなデータを安全に保持できる
  3. データのカプセル化が実現できる

いかがでしたか?クロージャは最初は難しく感じるかもしれませんが、使いこなせるようになると非常に便利な機能です。みなさんも、ぜひ実際にコードを書いて試してみてください!