今回の記事では、JavaScriptの少し難しい概念「クロージャ」について詳しく解説していきます。私もそうでしたが、初学者には難しい表現ですので、しっかりと噛み砕いて解説します。
さあ、OZくん。今日はJavaScriptのクロージャについて勉強していきましょう!
クロージャとは、簡単に言うと「関数と、その関数が作られた時の環境がセットになったもの」です。関数が引数や自分の外側で定義された変数を覚えていられる、とても便利な機能なんですよ。
えっ、なんですかそれ。関数が引数や変数を覚えるってどういうことですか?めっちゃ難しそう…
はい、確かに言葉だけだと分かりにくいですよね。じゃあ、具体的な例を見ながら理解していきましょう。まずはシンプルな挨拶をする関数から見てみましょう。
function makeGreeting(name) {
const message = "こんにちは、";
function greet() {
console.log(message + name + "さん!");
}
return greet;
}
const greetOz = makeGreeting("OZ");
greetOz(); // "こんにちは、OZさん!"と表示される
なんか関数の中に関数があってめっちゃごちゃごちゃしてんな〜。どないなってんのかさっぱりです…
じゃあ、このコードで起きていることを順番に見ていきましょう。
まず、外側のmakeGreeting
関数では3つのことが起きています
name
というパラメータを受け取ってmessage
という定数を定義して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()
を実行すると、
- 新しい
count
変数(値は0)が作られる - その
count
を覚えている関数が返される - でもまだカウントアップは実行されていない
つまり、この段階では、まだfunction()がリターンされただけで、実行されていません。
返された関数を実行して初めて、
count
が1増える- 増えた値が返される
という流れになります。
つまり:
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
は追加する数ですね?
そうですね。stock
にamount
を足して、結果をメッセージとして返しています。
たとえば、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個です」と表示されます。
なるほど!!残りの数もちゃんと計算されてますね!これ、めっちゃ実用的ですね!在庫数をプライベートに保持しながら、必要な操作だけできるようになってる!
そのとおりです。クロージャを使うと、このように変数を安全に管理しながら、必要な機能だけを提供できるです。
まとめ
クロージャの重要なポイント:
- 関数が、引数や自分の外側で定義された変数を覚えていられる
- プライベートなデータを安全に保持できる
- データのカプセル化が実現できる
いかがでしたか?クロージャは最初は難しく感じるかもしれませんが、使いこなせるようになると非常に便利な機能です。みなさんも、ぜひ実際にコードを書いて試してみてください!