順番に読み進めながら学べます

高階関数とコールバック — 関数を渡す・関数を返す

関数を引数として受け取る高階関数と、渡されて呼び出されるコールバックを扱います。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 が 3 回呼ばれる
1 件目のA-1101order にA-1101 が入るA-1101 ノート480 円 と表示2 件目のA-1102order にA-1102 が入るA-1102 ボールペン150 円 と表示3 件目のA-1103order にA-1103 が入るA-1103 付箋320 円 と表示
渡したのは printOrder 1 つで、呼び出しているのは forEach です。呼ばれるたびに、order に入る要素だけが入れ替わります

orders.forEach(printOrder) の行には、繰り返しの条件も添字も書かれていません。表示の形を変えたいときは printOrder だけを直せば済み、同じ関数を別の一覧にもそのまま渡せます。

forEach は途中で止められません

forEach の中では break を書けず、return を書いてもその 1 件分のコールバックを終えるだけで、残りの要素は続けて処理されます。目当ての要素が見つかった時点で打ち切りたい繰り返しは、for...ofbreak で書きます。

サポートへの問い合わせ一覧を、1 件ずつ表示します。tickets は宣言済みです。

① 1 件を「[T-01] 配送状況を知りたい(対応済み)」の形で表示する関数 printTicket を書いてください。未対応なら(未対応)と出します。

② printTicket を forEach に渡して、全件を表示してください。

③ 対応済みの件数を数えて、「対応済み ◯ 件」の形で表示してください。

(正しく実行できれば解説が表示されます)

JavaScript / TypeScript エディタ

コードを実行してください

判定を関数で受け取る — 自作 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);         // デスクマット
戻り値が要素の行き先を決める
在庫 4 のデスクマットstock > 0 を判定する4 > 0 でtruekept に入る在庫 0 のモニター台stock > 0 を判定する0 > 0 でfalsekept に入らない
在庫 4 の商品には true、在庫 0 の商品には false が返り、kept に入るのは前者だけです。残すかどうかは、渡した関数の戻り値で決まります

filterItems の中には「在庫」という語がひとつも出てきません。在庫の多い商品だけに絞りたくなっても、呼び出す行の判定を product.stock >= 5 に書き換えるだけで、filterItems には手を入れずに済みます。

配送予定から、条件に合う便の数を数えます。shipments は宣言済みです。

① 判定する関数を受け取り、条件に合う要素の件数を返す countIf を書いてください。

② 当日発送の便の件数を表示してください。

③ 送料が 4000 円以上の便の件数を表示してください。

④ 当日発送ではない便を判定する関数を変数 isNextDay に入れてから countIf に渡し、件数を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

作った判定をそのまま渡す — 関数を返す高階関数

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 つ目の引数に渡す
byCategory の中
  • category — 呼び出した行で "文房具" が入る
  • return (item) => ... — 比べずに、判定の関数を返す
返された判定の関数の中
  • itemfilterItems から渡された 1 件が入る
  • item.category === category — 覚えている category と見比べる
filterItems の中
  • judgebyCategory が返した判定の関数が入る
  • judge(item) — 要素を 1 件ずつ渡して判定を呼ぶ
categorybyCategory を呼んだ行で、itemfilterItems の中で決まります。判定を作る場所と、判定を呼ぶ場所は別です

byCategory("文房具") を実行した行ではまだ何も比べておらず、比較が動くのは filterItems の中で judge(item) が呼ばれたときです。3 件の商品なら同じ判定が 3 回呼ばれ、category は "文房具" のまま item だけが入れ替わります。

括弧なしで渡すと全件が残ります

filterItems(items, byCategory) と書くと、商品が category として byCategory に渡り、判定の結果として関数が返ります。関数は truthy(falsy 以外の値)なので、どの商品も残ります。渡すのは byCategory("文房具") が返した判定です。

会員限定の送料無料の対象を絞り込みます。orders と filterItems は宣言済みです。

① 判定を 2 つ受け取り、両方を満たすときだけ true を返す判定を返す both を書いてください。

② 5000 円以上の判定と、会員の判定を変数に入れてください。

③ ② を both で組み合わせて filterItems に渡し、残った注文の番号を 1 件ずつ表示してください。

④ ③ の判定を 2 件目の注文に使い、結果を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

骨組みを 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
骨組み 1 つに 3 つの料金関数を通す
sumShipping(orders,calcFee) は 1 つだけcalcFee にnormalFeecalcFee にexpressFeecalcFee にflatFee5000 円以上は 0未満は 500注文によらず800注文によらず300合計は 500合計は 1600合計は 600
calcFee に渡した関数によって、同じ 2 件の合計が 500・1600・600 に分かれます。sumShipping の中身は 3 回とも同じです

たとえば合計に上限を設けるような変更も、sumShipping を 1 か所直せば 3 通りの料金すべてに効きます。下の表は、この記事の 3 つの受け取る側が、コールバックをどう呼び、戻り値をどう使うかを並べたものです。

関数を受け取る側コールバックの呼び出し方コールバックの戻り値
forEach要素を 1 件ずつ渡して呼ぶ使わない
自作の filterItemsjudge(item) と要素を渡して呼ぶtrue の要素だけを集める
自作の sumShippingcalcFee(order) と注文を渡して呼ぶその 1 件の送料として足す

アルバイトの勤務記録から、1 件ごとの支給額を表示して合計を出します。shifts は宣言済みです。

① 勤務記録と支給額の計算関数を受け取り、1 件ずつ「◯ 時間: ◯ 円」と表示してから合計を返す payReport を書いてください。

② 時給 1100 円の計算を渡し、返った合計を「合計 ◯ 円」の形で表示してください。

③ 深夜のシフトだけ 1.25 倍になる計算を渡し、同じ形で合計を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1orders.forEach(printOrder) と書いたとき、printOrder を呼び出すのはどれですか?

Q2自作の filterItems で、渡した判定関数が false を返した要素はどうなりますか?

Q3filterItems(items, byCategory) のように括弧なしで渡すと、戻る配列はどうなりますか?