1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

reduce で書いたグループ分けを Object.groupBy に置き換えると、何が変わって何が変わらないか

1
Posted at

配列を「種類ごと」にまとめるとき、reduce で {} に詰めていく書き方があります。ES2024 で入った Object.groupBy を使うと、同じことを1行で書けます。

const items = [
  { name: "りんご", type: "fruit", price: 120 },
  { name: "にんじん", type: "vegetable", price: 80 },
  { name: "バナナ", type: "fruit", price: 100 },
];

// Before: reduce で {} に詰める
const byReduce = items.reduce((acc, x) => {
  (acc[x.type] ??= []).push(x);
  return acc;
}, {});

// After: Object.groupBy
const byGroup = Object.groupBy(items, (x) => x.type);

console.log(JSON.stringify(byReduce) === JSON.stringify(byGroup)); // true

中身は同じになりました。では、置き換えると何が変わって、何が変わらないのか。この記事では、Node.js v24.14.1 で実際に動かした結果をもとに整理します。コードは .mjs ファイルに保存して node ファイル名.mjs で実行しています。

Object.groupBy と Map.groupBy は、MDN の互換性データでは、どちらも Chrome 117、Firefox 119、Safari 17.4、Node.js 21.0.0 から使えるとされています。手元の WebKit(Safari と同じエンジン)でも、どちらも呼び出して結果が返りました。

変わること: 戻り値が「プロトタイプのないオブジェクト」になる

Object.groupBy の戻り値を console.log すると、先頭に [Object: null prototype] と出ます。ふつうのオブジェクトが Object.prototype から受け継いでいる hasOwnProperty や toString などのメソッドを、受け継いでいないという意味です。

console.log(byReduce.hasOwnProperty("fruit")); // true
byGroup.hasOwnProperty("fruit");
// TypeError(Node.js での表示): byGroup.hasOwnProperty is not a function

reduce 版で hasOwnProperty を使っていたコードは、置き換えるとエラーになりました。キーがあるかを調べるときは、Object.hasOwn か in を使います。

console.log(Object.hasOwn(byGroup, "fruit")); // true
console.log("fruit" in byGroup);              // true
console.log("toString" in byGroup);           // false(reduce 版なら true)

文字列の中に埋め込もうとしても、エラーになりました。JSON.stringify は、reduce 版と同じように文字列にできます。

console.log(`${byGroup}`);
// TypeError(Node.js での表示): Cannot convert object to primitive value

プロトタイプがないと何がうれしいのか

reduce 版で、グループのキーが constructor や toString になる場合を試しました。

const tags = [
  { name: "a", tag: "constructor" },
  { name: "b", tag: "toString" },
];

tags.reduce((acc, x) => {
  (acc[x.tag] ??= []).push(x);
  return acc;
}, {});
// TypeError(Node.js での表示): acc[x.tag].push is not a function

console.log(Object.groupBy(tags, (x) => x.tag));
// [Object: null prototype] {
//   constructor: [ { name: 'a', tag: 'constructor' } ],
//   toString: [ { name: 'b', tag: 'toString' } ]
// }

1件目の constructor で止まりました(toString でも同じ理由で止まります)。{} から始めた reduce 版では、acc.constructor に最初から Object.prototype から受け継いだ値(Object という関数)が見えているので、??= で空の配列が入らず、push できずに止まりました。プロトタイプのない Object.groupBy の戻り値なら、どんなキーでもぶつかりません。利用者が入力したタグ名やカテゴリ名でグループ分けするときは、この違いが効きます。

変わらないこと(reduce 版でも同じ)

ここから先は、Object.groupBy に特有のくせではなく、reduce 版でも同じように起きることです(キーの性質は、結果をオブジェクトで受け取る以上は避けられません)。置き換えても直りません。

キーは文字列になる

数字や true / false でグループ分けしても、キーは文字列になりました。

const nums = [1, 2, 3, 4];
const g2 = Object.groupBy(nums, (n) => n % 2);

console.log(Object.keys(g2));             // [ '0', '1' ](reduce 版も同じ)
console.log(Object.keys(g2).includes(0)); // false
console.log(Object.keys(g2).includes("0")); // true

g2[0] と書いても、0 が文字列 "0" に変換されて読めるので、気づきにくいところです。キーを取り出して数字として比べると、上のように食い違います。なお、最初に出てくるのは 1 なのに '0' が先に並んでいる理由は、次の項目で説明します。

0以上の整数として読めるキーは、小さい順に並ぶ

新しい年から順に並べたデータをグループ分けすると、キーは小さい順に並び替わりました。

const years = [{ y: 2026 }, { y: 2025 }, { y: 2024 }];
console.log(Object.keys(Object.groupBy(years, (x) => x.y)));
// [ '2024', '2025', '2026' ](reduce 版も同じ)

オブジェクトのキーは、0以上の整数として読めるものが先に小さい順で並び、ほかの文字列は作られた順に並ぶ決まりだからです。-1、1.5、01 のようなキーは並び替わらず、作られた順のままでした。

console.log(Object.keys(Object.groupBy([{ k: 2 }, { k: -1 }, { k: 1.5 }, { k: "01" }, { k: 1 }], (x) => x.k)));
// [ '1', '2', '-1', '1.5', '01' ]

このキーの並び順の決まりは、オブジェクトをループする3つの書き方と、for...in を配列に使ってはいけない理由 でも書きました。

1件もないグループは、キーごと存在しない

console.log(byGroup.meat); // undefined(reduce 版も同じ)
byGroup.meat.length;
// TypeError(Node.js での表示): Cannot read properties of undefined (reading 'length')

全部の種類を「0件」も含めて並べたいときは、先に空の配列を用意して上書きします。

const filled = {
  fruit: [],
  vegetable: [],
  meat: [],
  ...Object.groupBy(items, (x) => x.type),
};
console.log(filled.meat); // []

filled はふつうのオブジェクトになるので、前の章の hasOwnProperty も使えます(その代わり、プロトタイプのメソッドとキーがぶつかる可能性も戻ります)。

プロパティ名を打ち間違えると、"undefined" というグループになる

const u = Object.groupBy(items, (x) => x.category); // category というプロパティは無い
console.log(Object.keys(u)); // [ 'undefined' ]

エラーにはならず、全部が "undefined" という1つのグループにまとまりました。

中身はコピーではない

byGroup.fruit[0].price = 999;
console.log(items[0].price);              // 999
console.log(byGroup.fruit[0] === items[0]); // true

グループの中の要素は、元の配列と同じオブジェクトです(Map.groupBy でも同じでした)。グループ分けしたあとに値を書き換えると、元の配列の値も変わります。

キーの型と並び順を保ちたいなら Map.groupBy

Map.groupBy は、結果を Map で返します。キーの型と、最初に出てきた順が保たれました。

const m = Map.groupBy(items, (x) => x.price >= 100);
console.log([...m.keys()]);      // [ true, false ](真偽値のまま)
console.log(m.get(true).length); // 2
console.log(m.get("true"));      // undefined(文字列とは区別される)

console.log([...Map.groupBy(years, (x) => x.y).keys()]);
// [ 2026, 2025, 2024 ](元の並び=新しい順のまま)

ただし、並び順は「最初に出てきた順」です。新しい順に表示したいなら、元の配列を先に新しい順に並べておくか、グループ分けのあとで並べ替えます。

種類で分けた Map.groupBy(items, (x) => x.type) でも、1件もない "meat" は get すると undefined でした。また、JSON.stringify(m) は {} になり、中身が出ません。JSON にするなら Object.fromEntries(m) でオブジェクトに戻してから変換します(戻した時点で、キーはまた文字列になります。上の m なら [ 'true', 'false' ])。

オブジェクトをキーにすると、中身ではなく「同じオブジェクトかどうか」で分けられます。

const owner = { id: 1 };
const m2 = Map.groupBy([{ o: owner }, { o: owner }, { o: { id: 1 } }], (x) => x.o);
console.log(m2.size); // 2(中身が同じでも、別のオブジェクトは別のグループ)

使い分けの目安です。

  • キーが文字列で、そのまま JSON にしたり、byGroup.fruit のように読んだりするなら Object.groupBy
  • キーが数字・真偽値・オブジェクトで、型や出てきた順を保ちたいなら Map.groupBy

AI に置き換えを頼んだときの確認ポイント

  • 置き換え前のコードで、戻り値に hasOwnProperty を使っていなかったか(Object.hasOwn か in に)
  • 置き換え前から、キーを数字として比べたり、数字のキーの並び順に頼ったりしていなかったか(置き換えても直らない)
  • 0件のグループを、空の配列があるつもりで読んでいないか
  • Map.groupBy の結果を、そのまま JSON.stringify していないか

未経験者向けの講座を運営しています

未経験から Next.js + Supabase + Claude Code で Webアプリを公開するまで を、全24セッションで体系化した教材です。Claude Code を学習パートナーにする CLAUDE.md と学習モード(learner / developer)の設計までセットで含みます。

※ 中上級者には易しすぎる内容なので、初心者の知り合いへの紹介や社内研修の参考としてどうぞ。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?