クロージャで状態を閉じ込める
このレッスンで分かること
- 関数の中で作った変数が、関数を抜けたあとも生き残る仕組み
- 同じ工場から作った関数どうしが、別々の状態を持つこと
誰でも書き換えられる場所に置いてしまう
プロフィールサイトのメニューが今開いているかどうかを覚えておきたい、としましょう。素直に書くとこうなります。
JavaScript
let isOpen = false;
function toggleMenu() {
isOpen = !isOpen;
return isOpen;
}動きます。ただし isOpen はファイルのどこからでも書き換えられます。あとから誰かが別の場所で isOpen = false と書けば、メニューは開いたままなのに閉じている扱いになります。切り替えたいのは toggleMenu だけなのに、書き換えられる範囲だけがファイル全体に広がっている、という状態です。
変数を置く場所を決めるときは、「誰が読めるか」より先に「誰が書き換えられるか」を考えます。書き換える人がひとりだけなら、その人の内側に置くのがいちばん壊れにくくなります。
関数の中に置いて、関数ごと返す
書き換えるのが toggleMenu だけなら、isOpen をその外側の関数の中に入れてしまいます。
JavaScript
function createToggle() {
let open = false;
return function () {
open = !open;
return open;
};
}
const toggleMenu = createToggle();
console.log(toggleMenu()); // true
console.log(toggleMenu()); // falsecreateToggle は、呼ばれると let open = false を用意し、open を触る関数を作って返します。返した時点で createToggle の実行は終わりますが、open は消えません。返された関数がまだ open を使うので、JavaScript は残しておきます。この「返された関数が、生まれた場所の変数を連れて出ていく」形をクロージャと呼びます。
大事なのは、open がもうどこからも名前で呼べないことです。値を変える方法は toggleMenu() を呼ぶことだけになりました。値の寿命はそのままに、触れる範囲だけを狭めた、と言い換えられます。
| 置き場所 | 外から書き換えられるか | 値の寿命 |
|---|---|---|
ファイル直下の let | どこからでもできる | ページを閉じるまで |
関数の中の let (返さない) | できない | 関数を抜けた時点で消える |
関数の中の let (関数ごと返す) | できない | 返した関数が生きている間 |
3 行目だけが「消えない」と「触れない」を両立しています。クロージャを使う理由はここに尽きます。
工場を呼ぶたびに、別の箱ができる
createToggle を 2 回呼ぶと、open も 2 つできます。呼び出しのたびに let open = false が新しく実行されるからです。
JavaScript
const toggleMenu = createToggle();
const togglePanel = createToggle();
console.log(toggleMenu()); // true
console.log(toggleMenu()); // false
console.log(togglePanel()); // truetoggleMenu を 2 回呼んでも togglePanel は影響を受けません。作品カードごとに状態を持ちたいときは、カードの数だけ工場を呼べばよい、ということです。グローバル変数でこれをやろうとすると、カードの数だけ変数名を考えることになります。共有されるのは「作りかた」であって「値」ではありません。
なお、閉じ込めた値はコンソールから覗けません。動きを確かめたいときは、返ってきた関数を呼んだ戻り値をそのまま見ます。今回の課題も、その戻り値で正しさを測ります。
よくある間違い
- 工場を呼ばずに名前だけ代入する —
const toggleMenu = createToggle;と書くと、返ってくるのは工場そのものです。使うのはcreateToggle()の戻り値のほうだ、と覚えます letを工場の外に出してしまう — 外に出すと 2 つ作っても箱はひとつしかなく、toggleMenuとtogglePanelが同じ値を取り合います。宣言は必ず工場の内側に書きます
要件
- 関数名は trackViews とし、引数は startCount (数値) と times (数値) の 2 つにする
- 関数の中で、カウンタ用の関数を返す関数を定義して使う。数える変数は返した関数の外側に置き、関数の外から触れないようにする
- カウンタは呼ばれるたびに 1 増え、増えたあとの値を返す。startCount が 0 なら 1 回目の戻り値は 1 になる
- 戻り値は times 個の数値が入った配列にする。times が 0 のときは空の配列を返す
入出力例
trackViews(0, 3) → [1,2,3]
trackViews(120, 2) → [121,122]
trackViews(7, 1) → [8]
trackViews(5, 0) → []
trackViews(100, 5) → [101,102,103,104,105]