Q1orders.forEach(printOrder) と書いたとき、printOrder を呼び出すのはどれですか?
高階関数とコールバック — 関数を渡す・関数を返す
関数を引数として受け取る高階関数と、渡されて呼び出されるコールバックを扱います。forEach での 1 件ずつの処理、判定を受け取る自作 filter、関数を返す関数で作った判定の受け渡しまで確かめます。
同じ一覧に対して、1 件ずつ表示するだけの処理、条件で絞る処理、金額を集計する処理と、やりたいことが少しずつ違う場面があります。共通しているのは繰り返しの骨組みで、違うのは 1 件分の処理だけです。
この記事では、関数を引数として受け取る高階関数と、渡されて呼び出される側のコールバックを扱います。
処理を引数として渡す — コールバックと forEach
注文の一覧を 1 件ずつ表示したいとします。for...of で書けば表示はできますが、一覧を表示するたびに同じ繰り返しの行を書くことになり、変わるのは 1 件分の表示の形だけです。
高階関数(関数を引数として受け取るか、関数を返す関数)を使うと、繰り返しの骨組みと 1 件分の処理を分けられます。引数として渡され、受け取った側から呼び出される関数をコールバックと呼びます。配列のメソッド forEach は、渡されたコールバックを要素の数だけ呼びます。
const orders = [
{ id: "A-1101", item: "ノート", amount: 480 },
{ id: "A-1102", item: "ボールペン", amount: 150 },
{ id: "A-1103", item: "付箋", amount: 320 },
];
// 1 件分の表示だけを関数にする
const printOrder = (order) => {
console.log(`${order.id} ${order.item} ${order.amount} 円`);
};
// 渡すのは関数の名前だけ。呼び出すのは forEach の側
orders.forEach(printOrder);
// A-1101 ノート 480 円
// A-1102 ボールペン 150 円
// A-1103 付箋 320 円
printOrder 1 つで、呼び出しているのは forEach です。呼ばれるたびに、order に入る要素だけが入れ替わります。orders.forEach(printOrder) の行には、繰り返しの条件も添字も書かれていません。表示の形を変えたいときは printOrder だけを直せば済み、同じ関数を別の一覧にもそのまま渡せます。
forEach は途中で止められません
forEach の中では break を書けず、return を書いてもその 1 件分のコールバックを終えるだけで、残りの要素は続けて処理されます。目当ての要素が見つかった時点で打ち切りたい繰り返しは、for...of と break で書きます。
判定を関数で受け取る — 自作 filter と真偽値
在庫のある商品だけ、次は 3000 円以上の商品だけ、と条件を変えて一覧を絞りたいとします。条件ごとにループを書くと、集めるための配列を用意して push する行まで、そっくり同じものが増えていきます。
変わるのは条件を書いた 1 行だけなので、そこを判定の関数として受け取り、戻り値が true の要素だけを集めます。コールバックに何を何番目で渡すかは、コールバックを受け取って呼び出す側が決めます。下の filterItems は、判定の関数に要素を 1 つだけ渡します。
const products = [
{ name: "デスクマット", stock: 4 },
{ name: "モニター台", stock: 0 },
{ name: "ケーブルボックス", stock: 9 },
];
// 判定する関数を受け取り、true が返った要素だけを集める
function filterItems(items, judge) {
const kept = [];
items.forEach((item) => {
if (judge(item)) {
kept.push(item);
}
});
return kept;
}
const inStock = filterItems(products, (product) => product.stock > 0);
console.log(inStock.length); // 2
console.log(inStock[0].name); // デスクマット
true、在庫 0 の商品には false が返り、kept に入るのは前者だけです。残すかどうかは、渡した関数の戻り値で決まります。filterItems の中には「在庫」という語がひとつも出てきません。在庫の多い商品だけに絞りたくなっても、呼び出す行の判定を product.stock >= 5 に書き換えるだけで、filterItems には手を入れずに済みます。
作った判定をそのまま渡す — 関数を返す高階関数
filterItems に「文房具だけ」「食器だけ」と、比べるカテゴリだけが違う判定を何度も渡す場面があります。呼び出すたびに判定の式を書き並べると、比べるキー名を直したいときに、その全部を直すことになります。
高階関数のもう一方の形は、関数を返す関数です。前の記事の関数ファクトリと同じ形で、カテゴリを受け取って判定の関数を返す byCategory を作ります。返された判定はクロージャとして category を覚えているので、filterItems にそのまま渡せます。
const items = [
{ name: "ノート", category: "文房具" },
{ name: "マグカップ", category: "食器" },
{ name: "ボールペン", category: "文房具" },
];
// カテゴリを受け取り、そのカテゴリかどうかを判定する関数を返す
function byCategory(category) {
return (item) => item.category === category;
}
// この時点ではまだ比べておらず、返るのは判定の関数
console.log(typeof byCategory("文房具")); // function
// 前の章で作った filterItems に、返ってきた判定をそのまま渡す
console.log(filterItems(items, byCategory("文房具")).length); // 2
console.log(filterItems(items, byCategory("食器")).length); // 1
filterItems(items, byCategory("文房具"))— 判定を作って、2 つ目の引数に渡す
category— 呼び出した行で "文房具" が入るreturn (item) => ...— 比べずに、判定の関数を返す
item—filterItemsから渡された 1 件が入るitem.category === category— 覚えている category と見比べる
judge—byCategoryが返した判定の関数が入るjudge(item)— 要素を 1 件ずつ渡して判定を呼ぶ
category は byCategory を呼んだ行で、item は filterItems の中で決まります。判定を作る場所と、判定を呼ぶ場所は別です。byCategory("文房具") を実行した行ではまだ何も比べておらず、比較が動くのは filterItems の中で judge(item) が呼ばれたときです。3 件の商品なら同じ判定が 3 回呼ばれ、category は "文房具" のまま item だけが入れ替わります。
括弧なしで渡すと全件が残ります
filterItems(items, byCategory) と書くと、商品が category として byCategory に渡り、判定の結果として関数が返ります。関数は truthy(falsy 以外の値)なので、どの商品も残ります。渡すのは byCategory("文房具") が返した判定です。
骨組みを 1 つにする — 処理を差し替える汎用関数
送料の合計を出す処理を、通常便と速達で 2 つ書いたとします。繰り返しも合計を入れる変数も同じで、違うのは 1 件あたりの料金を出す式だけです。この形では、片方だけ直してもう片方を直し忘れることが起きます。
違う部分を引数の関数にして、骨組みを 1 つにまとめます。sumShipping は注文の配列と料金を計算する関数を受け取り、注文を 1 件ずつ渡して戻り値を足します。呼び出す側は料金の式だけを書き、受け取る側は繰り返しと合計だけを持ちます。
const shippingOrders = [{ id: "A-1101", total: 6200 }, { id: "A-1102", total: 3400 }];
// 骨組みは 1 つ。料金の出し方だけを関数で受け取る
function sumShipping(orders, calcFee) {
let total = 0;
for (const order of orders) {
total = total + calcFee(order);
}
return total;
}
const normalFee = (order) => (order.total >= 5000 ? 0 : 500);
const expressFee = () => 800; // order を使わないので引数を書かなくてよい
const flatFee = () => 300;
console.log(sumShipping(shippingOrders, normalFee)); // 500
console.log(sumShipping(shippingOrders, expressFee)); // 1600
console.log(sumShipping(shippingOrders, flatFee)); // 600
calcFee に渡した関数によって、同じ 2 件の合計が 500・1600・600 に分かれます。sumShipping の中身は 3 回とも同じです。たとえば合計に上限を設けるような変更も、sumShipping を 1 か所直せば 3 通りの料金すべてに効きます。下の表は、この記事の 3 つの受け取る側が、コールバックをどう呼び、戻り値をどう使うかを並べたものです。
| 関数を受け取る側 | コールバックの呼び出し方 | コールバックの戻り値 |
|---|---|---|
| forEach | 要素を 1 件ずつ渡して呼ぶ | 使わない |
| 自作の filterItems | judge(item) と要素を渡して呼ぶ | true の要素だけを集める |
| 自作の sumShipping | calcFee(order) と注文を渡して呼ぶ | その 1 件の送料として足す |
理解度チェック
まずは1問ずつ答えてみましょう。
Q2自作の filterItems で、渡した判定関数が false を返した要素はどうなりますか?
Q3filterItems(items, byCategory) のように括弧なしで渡すと、戻る配列はどうなりますか?