副作用のない関数
このレッスンで分かること
- 同じ引数なら必ず同じ戻り値になる関数の書きかた
- 受け取った配列やオブジェクトを書き換えずに、新しいものを返す手順
2 回呼ぶと答えが変わる
プロフィールに載せるスキル一覧のレベルを、まとめて 1 つ上げてみます。素直に書くとこうなります。
JavaScript
function levelUp(list) {
list.forEach(function (skill) {
skill.level = skill.level + 1;
});
return list;
}
const skills = [{ name: "HTML", level: 3 }];
console.log(levelUp(skills)[0].level); // 4
console.log(levelUp(skills)[0].level); // 51 回目と 2 回目で答えが違います。関数が skills の中身を書き換えているので、次に呼ぶときの入力がもう別物になっているからです。
これはテストの書けない関数です。「何を返すか」を答えるのに、それまでの呼び出し履歴が必要になるからです。書けたとしても、実行の順番を変えた瞬間に落ちます。裏を返せば、引数と戻り値だけで正しさが決まる関数なら、画面もサーバーも用意せずに確かめられます。このコースの課題が戻り値だけで採点できているのは、書いてもらう関数がその形だからです。
書き換えるのをやめて、作り直す
map は元の配列を変えずに新しい配列を返すので、そのまま使えます。
JavaScript
function levelUp(list) {
return list.map(function (skill) {
return { ...skill, level: skill.level + 1 };
});
}
const skills = [{ name: "HTML", level: 3 }];
console.log(levelUp(skills)[0].level); // 4
console.log(levelUp(skills)[0].level); // 4
console.log(skills[0].level); // 3何度呼んでも 4 で、元の skills は 3 のままです。中の skill をそのまま返さず、スプレッドで写しを作っているところが要点です。写しを作らずに返すと、配列は新しくても、中のオブジェクトは元と同じものを指したままになります。
| 元を書き換えるもの | 新しいものを返すもの |
|---|---|
push pop splice | concat slice スプレッド |
sort reverse | 写しを作ってから sort |
skill.level = 4 という代入 | スプレッドで写しを作る |
第2章で sort が元の配列を並び替えてしまう話をしました。原因は同じで、対処も同じです。まず写しを作り、写しを並び替えます。
引数以外を見ない
もうひとつの条件が、引数以外から値を読まないことです。
JavaScript
let taxRate = 0.1;
function withTax(price) {
return Math.round(price * (1 + taxRate)); // 外の値を読んでいる
}taxRate が書き換われば、同じ price でも答えが変わります。引数で受け取る形にすれば、この関数は引数だけで決まるようになります。Math.random や new Date も、呼ぶたびに答えが変わるので同じ問題を抱えます。関数の中で呼ばず、呼び出し側で作った値を引数で受け取ってください。
書き換えを一切しないプログラムは作れません。画面への描画、通信、保存は必ず残ります。無くすのではなく、計算する関数は書き換えないものにそろえ、書き換えるのは最後の 1 か所に集める、という分けかたをします。
よくある間違い
- 配列だけ写して中身を写し忘れる —
[...list]で配列は新しくなりますが、中の要素は元と同じオブジェクトです。そこに代入すると、元のデータも一緒に変わります sortを写す前に呼ぶ —sortは元の配列を並び替えて、その同じ配列を返します。受け取った側からは新しい配列に見えるので、いちばん気付きにくい形です- 集計しながら外の変数に足していく — 関数の外に置いた
let totalに足すと、2 回呼んだだけで合計が 2 倍になります。合計は関数の中で 0 から始めるか、reduceで作ります
要件
- 関数名は applyBonus とし、引数は works (作品の配列) と bonus (数値) の 2 つにする
- views に bonus を足した新しい配列を作る処理を、works を書き換えずに行う。同じ works に対して 2 回行う
- 戻り値は first と second と original の 3 つのキーを持つオブジェクトにする。値はいずれも views の数値だけを並べた配列にする
- first は 1 回目の結果、second は 2 回目の結果、original は処理を終えた時点の works の views にする。works が空なら 3 つとも空の配列になる