hbsnow.dev

reduce の使いどころ

まとめ

  • 使う場面は次の二点
    • 配列の合計を計算する
    • オブジェクトの配列から最大、最小値をもつオブジェクトを取得する
  • ループとして使わないようにする

まえがき

MDN にわかりやすいサンプルもありますが、自分の整理もかねて reduce のよくある使用方法と、使うべき場面について考えてみます。

reduce の使用例

ここでは上記5つの例をみていきます。

1. 配列の合計を計算する

reduce の例として、よくあるものが合計値の計算です。

[3, 7, 1, 2].reduce((accumulator, currentValue) => accumulator + currentValue); // => 13

こういった単純な一次配列を合計できるし、次のようなオブジェクトの特定プロパティの合計を求めることもできます。

const cart = [
  { itemId: 1, quantity: 3 },
  { itemId: 2, quantity: 2 },
  { itemId: 3, quantity: 1 },
];

const sum = cart.reduce((accumulator, currentValue) => {
  return accumulator + currentValue.quantity;
}, 0);

console.log(sum); // => 6

また、特定のプロパティ名のある値を除外するようなこともできます。以下は、itemIdが2であれば合計値に含めないコードです。

const sum = cart.reduce((accumulator, currentValue) => {
  return currentValue.itemId !== 2
    ? accumulator + currentValue.quantity
    : accumulator;
}, 0);

console.log(sum); // => 4

2. オブジェクトの配列から最大、最小値をもつオブジェクトを取得する

cartquantity の最大、あるいは最小の値をもつオブジェクトを取得したい場合。

const maxItem = cart.reduce((accumulator, currentValue) =>
  accumulator.quantity > currentValue.quantity ? accumulator : currentValue
);
console.log(sum); // => { itemId: 1, quantity: 3 }

const minItem = cart.reduce((accumulator, currentValue) =>
  accumulator.quantity < currentValue.quantity ? accumulator : currentValue
);
console.log(sum); // => { itemId: 3, quantity: 1 }

単純に値だけを求めたいのであれば Math.max(...cart.map(item => item.quantity)) とすればいいでしょう。

3. 集計する

SQLでいう group by のような集計を取りたいときに便利です。

const items = [
  { amount: 500, taxRate: 8 },
  { amount: 1000, taxRate: 8 },
  { amount: 1200, taxRate: 10 },
  { amount: 0, taxRate: 0 },
];

このような配列から taxRate ごとに集計をとりたくなったときには、次のように記述できます。

const groupByTaxRate = (items) => {
  const taxes = items.reduce((accumulator, currentValue) => {
    const key = currentValue.taxRate;
    accumulator[key] = !accumulator[key]
      ? currentValue.amount
      : accumulator[key] + currentValue.amount;

    return accumulator;
  }, {});

  return Object.keys(taxes).map((taxKey) => ({
    taxRate: parseInt(taxKey),
    amount: taxes[taxKey],
  }));
};

groupByTaxRate(items) の出力結果は次の通り。

[
  { taxRate: 0, amount: 0 },
  { taxRate: 8, amount: 1500 },
  { taxRate: 10, amount: 1200 },
]

reduce を使わなかった場合。

const groupByTaxRate = (items) => {
  let taxes = {};
  for (const item of items) {
    if (!taxes[item.taxRate]) {
      taxes[item.taxRate] = item.amount;
      continue;
    }
    taxes[item.taxRate] = taxes[item.taxRate] + item.amount;
  }

  return Object.keys(taxes).map((taxKey) => ({
    taxRate: parseInt(taxKey),
    amount: taxes[taxKey],
  }));
};

こんな感じになるでしょうか。どちらが読みやすいかはちょっと迷います。

4. flatten

flat が使えるのであればこんなことをする必要はありません。

const arr = [1, 2, [3, 4]];
console.log(arr.flat()); // => [1, 2, 3, 4]

これを reduce で同じことをしてみます。

console.log(
  arr.reduce(
    (accumulator, currentValue) => accumulator.concat(currentValue),
    []
  )
); // => [1, 2, 3, 4]

こうなります。

ただ、2次元より大きな配列に対応するためにはもう少し複雑な処理が必要になって、MDN の flat の項目にもあるのでそちらにまかせることとします。

5. async/await で配列の順次処理

これについては別記事 JavaScript の async/await を forEach で使ったらハマった話を書いたのでそちらを参照してください。

reduce を使うべきか

Is reduce() bad? - HTTP 203
In this episode, Jake and Surma discuss the array function reduce(). Is it good to use it? Is it too “smart”? Does it increase or decrease readability? Does ...
Is reduce() bad? - HTTP 203 favicon https://www.youtube.com/watch?v=qaGjS7-qWzg
Is reduce() bad? - HTTP 203

Jake Archibald が Twitter で reduce について言及して 少し話題になっていて、自分も考えさせられました。合計や最大値を求めるといった以外のループとしての用途で使うことってそれなりにあるんですよね。

前述のサンプルだと『async/awaitで配列の順次処理』がまさにこれに該当するし、『配列の合計を計算する』の最後の例。

const sum = cart.reduce((accumulator, currentValue) => {
  return currentValue.itemId !== 2
    ? accumulator + currentValue.quantity
    : accumulator;
}, 0);

これは合計のために reduce は残りますが、次のように書き直せます。

const sum = cart
  .filter((item) => item.itemId !== 2)
  .reduce((accumulator, currentValue) => {
    return accumulator + currentValue.quantity;
  }, 0);

reduce をループとして使う例

reduce をループとして使うシンプルな例を示していきます。

const items = [
  { amount: 500, taxRate: 8 },
  { amount: 1000, taxRate: 8 },
  { amount: 1200, taxRate: 10 },
  { amount: 0, taxRate: 0 },
];

さきほどのサンプルの配列で、amountが1000以上のものについてそれぞれ +100 した結果のオブジェクトの配列。

[
  { amount: 1100, taxRate: 8 }
  { amount: 1300, taxRate: 10 }
]

このような結果がほしいとき、reduce を使った場合にはこうなります。

const result = items.reduce((accumulator, currentValue) => {
  if (currentValue.amount >= 1000) {
    accumulator.push({
      ...currentValue,
      amount: currentValue.amount + 100,
    });
  }

  return accumulator;
}, []);

でもこれは実際にこんな書き方をする必要はありません。

const result = items
  .filter((item) => item.amount >= 1000)
  .map((item) => ({
    ...item,
    amount: item.amount + 100,
  }));

mapfilter で書き直すことができます。

このコードを reduce で記述するのはなんだか賢くなった気分になる、というのは確かにありますが、この例は明らかに mapfilter のほうが何をしたいのかが明確で可読性が高いはずです。

オブジェクトの指定 key を Omit する

オブジェクトで特定keyを除外したいとき、reduceを使うことができます。

const example = {
  foo: 0,
  bar: 1,
  baz: 2,
};

const result = Object.keys(example)
  .filter((key) => !["foo", "bar"].includes(key))
  .reduce(
    (newObj, key) => ({
      ...newObj,
      [key]: example[key],
    }),
    {}
  );

console.log(result); // => { baz: 2 }

でも、これも reduce を使う必要はありません。

Object.fromEntries(
  Object.entries(example).filter(([key]) => !["foo", "bar"].includes(key))
);

Object.fromEntriesObject.entries で記述できるし、この例だともっとシンプルに次のように書くことができます。

const { foo, bar, ...result } = example;

これで問題ないはずです。

また、こういった配列処理の例は動画の最後に紹介されている Underdash にまとまっているので、ここでもおすすめしておきます。