JavaScript初心者がUdemyでToDoリストでCRUDを学ぼうとしたら、再帰とクロージャの沼にハマって納得しました。
Udemyで学んだToDリストを自分なりに温泉Wishリストに変形して1から書いてみたら、関数が再帰していることに気が付き、なんで無限ループにならないのかと疑問を感じました。
無限ループにならない理由はクリックイベントリスナーがついているので、ユーザーがクリックするまで関数が実行されないためでした。
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JavaScriptで温泉ウィッシュリストを作る練習</title>
</head>
<body>
<h1>温泉ウィッシュリスト</h1>
<div id="input-area">
<input id="input_form" placeholder="行きたい温泉を入力" />
<button id="addBtn">追加</button>
</div>
<div id="incomplete-area">
<h1>行ってみたい温泉</h1>
<ul id="wish-list">
<!-- <li>
<div class="list-row">
<p>運動</p>
<button>行った</button>
<button>削除</button>
</div>
</li>
<li>
<div class="list-row">
<p>旅行</p>
<button>行った</button>
<button>削除</button>
</div>
</li> -->
</ul>
</div>
<div id="complete-area">
<h1>いったことがある温泉</h1>
<ul id="done_list">
<!-- <li>
<div class="list-row">
<p>Udemy勉強</p>
<button>戻す</button>
</div>
</li> -->
</ul>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
index.js
import './style.css';
// 追加ボタンを押した時に実行したい関数を定義
const onClickAdd = () => {
// inputタグに入力された文字列を取得
const onsen = document.getElementById('input_form').value;
document.getElementById('input_form').value = '';
createWishList(onsen);
};
// いってみたいリストの1行を作成する関数
const createWishList = (onsen) => {
// 追加する要素を作成
const li = document.createElement('li');
const div = document.createElement('div');
div.classList.add('list-row');
const p = document.createElement('p');
p.textContent = onsen;
// 削除ボタンを作成
const goBtn = document.createElement('button');
goBtn.textContent = '行った';
// 行ったボタンの機能を追加
goBtn.addEventListener('click', () => {
const moveTarget = goBtn.closest('li');
goBtn.nextSibling.remove();
goBtn.remove();
// 戻すボタンを作成
const backButton = document.createElement('button');
backButton.textContent = '戻す';
// 戻すボタンの機能(未来のイベントを作る)
backButton.addEventListener('click', () => {
createWishList(onsen);
backButton.closest('li').remove();
});
moveTarget.firstElementChild.appendChild(backButton);
const ul = document.getElementById('done_list');
// console.log(ul);
ul.appendChild(moveTarget);
});
// 削除ボタンを作成
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '削除';
// 削除ボタンの機能を追加
deleteBtn.addEventListener('click', () => {
deleteBtn.closest('li').remove();
});
// 要素の階層を作成
div.appendChild(p);
div.appendChild(goBtn);
div.appendChild(deleteBtn);
li.appendChild(div);
// console.log(li);
const ul = document.getElementById('wish-list');
ul.appendChild(li);
};
// 追加ボタンにイベントリスナーさんを追加しみはらせる
document.getElementById('addBtn').addEventListener('click', onClickAdd);
style.css
html {
color: #666;
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
}
h1 {
font-weight: bold;
font-size: 1.6rem !important;
margin-top: 0px;
text-align: center;
}
button {
background-color: lightgrey;
border-radius: 6px;
border: none;
padding: 6px 14px;
}
button:hover {
background-color: burlywood;
}
input {
padding: 6px 12px;
border-radius: 6px;
border: none;
margin-right: 6px;
}
.list-row {
display: flex;
align-items: center;
gap: 10px;
}
#input-area {
background-color: bisque;
padding: 10px;
border-radius: 6px;
margin-bottom: 16px;
}
#incomplete-area {
border-radius: 6px;
border: 2px solid burlywood;
padding: 16px;
margin-bottom: 16px;
min-height: 50px;
}
#complete-area {
border-radius: 6px;
background-color: bisque;
padding: 16px;
margin-bottom: 16px;
min-height: 50px;
}
1. 「再帰」とはどういう意味?
プログラミングにおける再帰(Recursion)とは、 「ある関数の中で、自分自身の関数を呼び出すこと」 を指します。
今回のコードは、まさにその形になっています。
createWishList の中で
backButton(戻すボタン)を作り
そのボタンが押されたら、また createWishList を実行する
2. なぜこの方法(再帰)を使うのか?
この「戻すボタン」の処理は 「卵が先か、鶏が先か」 みたいな状態になっているからです。
問題: createWishList 関数を作るときには、まだ「戻すボタン」は存在しない。
解決: でも、「戻すボタン」を押したときにやりたいことは、結局「createWishList 関数をもう一度動かすこと」そのもの。
そこで、 「あ、さっき作った自分(関数)をもう一回使い回せばいいじゃん!」 と考えるのが、この再帰的な書き方です。これによって、同じ処理(タグを作って、クラスをつけて、イベントをつけて…)を二度書かなくて済むようになります。
3. もし再帰を使わないと
もし「戻すボタンの作成」を createWishList の外に出そうとすると、逆に管理が大変になります。
外に出した場合:「どのボタン」が「どの温泉名」を「どこのリスト」に戻すべきか、毎回探し回る処理が必要になります。
中に入れた場合(今のコード):関数が動いた瞬間に、その時の onsen(温泉名)という変数をボタンが記憶(保持)してくれます。
この「関数が実行された時の環境を、中身の処理がずっと覚えている」仕組みを専門用語で クロージャ(Closure) と呼びます。今の実装は、このクロージャーという仕組みを使っています。
4.クロージャーとは
クロージャは単なる「関数の種類」ではなく、 「関数とその関数が作られた時の環境(変数など)をセットで閉じ込めたもの」という仕組み(現象) のことです。
「クロージャ」という言葉、なんだかイメージしにくいものです。
結論から言うと、クロージャは単なる「関数の種類」ではなく、「関数とその関数が作られた時の環境(変数など)をセットで閉じ込めたもの」という仕組み(現象) のことです。
わかりやすいイメージ:「記憶を持った関数」
普通、関数の中で作った変数は、その関数の処理が終わると消えてしまいます(使い捨て)。 しかし、クロージャという仕組みを使うと、「関数が死んでも、その中身(データ)だけは、特定の場所で生き続ける」 という不思議なことが起こります。
今回のコードで言うと、ここがクロージャの恩恵を受けている部分です:
const createWishList = (onsen) => { // 外側の関数(親)
// ...省略...
backButton.addEventListener('click', () => { // 内側の関数(子)
// この関数は「onsen」という名前をずっと覚えている!
createWishList(onsen);
});
};
- createWishList("草津温泉") として実行。
- その瞬間、この関数の中に「草津温泉」という文字が保存されます。
- 関数自体の処理は終わりますが、「戻すボタン」の中のクリックイベント(子)が、「草津温泉(onsen変数)」をギュッと握りしめて離しません。
数分後にボタンが押された時、すでに終わったはずの createWishList の時のデータを引っ張り出して使える。
なぜ「クロージャ」と呼ぶのか?
「Closure(閉鎖・閉じ込める)」という名前の通り、 変数を外から触れないように「閉じ込める」 からです。
もし onsen という変数を関数の外(グローバル)に置いてしまうと、他の温泉を追加した時に名前が上書きされて、どの「戻すボタン」を押しても最後に登録した温泉になってしまいます。
クロージャのおかげで、「それぞれのボタンが、自分専用の温泉名を、自分だけのポケットの中に閉じ込めて持っている」 状態になれる。
今回勉強したUdemyコース
【React18対応】モダンJavaScriptの基礎から始める挫折しないためのReact入門
https://www.udemy.com/course/modern_javascipt_react_beginner/
わかりやすくておすすめです。
