map()は各要素を変換して同じ長さの新配列を返し、filter()は条件に合う要素だけの新配列を返し、reduce()は要素を順に処理して1つの結果へ集約します。まず「変換・抽出・集約」のどれかを決めると選びやすくなります。
この記事は3メソッドの役割を比較する配列処理の親記事です。存在・全件判定はsome・every、ネスト解除はflat・flatMap、初期配列作成はfill・Array.fromへ役割を分けています。
map・filter・reduceはどう選ぶ?
| 質問 | メソッド | 結果 | 長さ |
|---|---|---|---|
| 全要素を別の値へ変えたい? | map() |
新しい配列 | 原則同じ |
| 条件に合う要素だけ残したい? | filter() |
新しい配列 | 同じか短い |
| 合計・objectなど1つへまとめたい? | reduce() |
任意の1値 | 該当なし |
| 1件でも該当するか知りたい? | some() |
boolean | 該当なし |
3メソッドともcallbackを使いますが、目的が違います。戻り値を使わず副作用だけを起こすなら、これらよりfor...ofが意図を表す場合があります。関数を引数に受け取る仕組みそのものは第一級関数と高階関数で説明しています。
map()は全要素を変換する
map(callback)は各要素にcallbackを実行し、その戻り値を同じindexの要素にした新配列を返します。
const products = [
{ id: 1, price: 1200 },
{ id: 2, price: 800 },
];
const labels = products.map((product) => ({
id: product.id,
label: `${product.price.toLocaleString("ja-JP")}円`,
}));
console.log(labels);
元配列の配列構造は変わりませんが、要素がobjectなら参照を共有し得ます。callback内でproduct.price = ...のように変更すると元objectも変わります。変更せず新しいobjectを返すと追跡しやすくなります。値渡しと参照渡しの違いは引数に値を渡すときと参照先を渡すときの挙動の差を参照してください。
map()でundefinedが並ぶ原因
波括弧を使うarrow functionでreturnを書き忘れると、各callbackの戻り値はundefinedです。
const wrong = [1, 2, 3].map((value) => {
value * 2;
});
const correct = [1, 2, 3].map((value) => {
return value * 2;
});
console.log(wrong); // [undefined, undefined, undefined]
console.log(correct); // [2, 4, 6]
filter()は条件に合う要素を抽出する
filter(callback)はcallbackがtruthyを返した元要素だけを新配列へ入れます。値を変換するメソッドではありません。
const products = [
{ id: 1, stock: 3 },
{ id: 2, stock: 0 },
{ id: 3, stock: 8 },
];
const available = products.filter((product) => product.stock > 0);
console.log(available.map((product) => product.id)); // [1, 3]
1件だけ必要ならfind()、有無だけならsome()を選ぶと不要な配列を作りません。
filter(Boolean)で消える値を理解する
filter(Boolean)はfalsyな値をすべて除きます。undefinedやnullだけでなく、数値0、空文字、falseも消えます。
console.log([0, 1, "", "ok", false].filter(Boolean));
// [1, "ok"]
const withoutNullish = [0, null, 1, undefined].filter(
(value) => value !== null && value !== undefined,
);
console.log(withoutNullish); // [0, 1]
0や空文字が有効なデータなら、除外条件を明示します。
reduce()は配列を1つの結果へ集約する
reduce(callback, initialValue)はaccumulatorへ各要素の結果を積み上げます。合計だけでなく、object、Map、配列など任意の値へ集約できます。
const cart = [
{ price: 1200, quantity: 2 },
{ price: 800, quantity: 1 },
];
const total = cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
console.log(total); // 3200
callbackの第一引数がaccumulator、第二引数が現在要素です。最後の0が初期値です。
reduce()では初期値を省略しない
初期値を省略すると最初の要素がaccumulatorになり、callbackは2要素目から始まります。空配列では初期値がないためTypeErrorになります。型と空配列の挙動を安定させるため、通常は初期値を明示します。
console.log([].reduce((sum, value) => sum + value, 0)); // 0
// TypeError
// [].reduce((sum, value) => sum + value);
reduce()で件数をobjectへまとめる
categoryごとの件数のように、配列からindexを作る用途にも使えます。
const posts = [
{ category: "javascript" },
{ category: "git" },
{ category: "javascript" },
];
const counts = posts.reduce((result, post) => {
const current = result[post.category] ?? 0;
return {
...result,
[post.category]: current + 1,
};
}, {});
console.log(counts); // { javascript: 2, git: 1 }
大きなデータで毎回objectをcopyする設計が必要かは要件次第です。まず正しい結果と読みやすさを確認し、performance問題を計測できた場合に実装を見直します。
map()とfilter()を連結する
処理の段階が「抽出してから変換」と読めるならchainで表せます。
const visibleTitles = posts
.filter((post) => post.status === "published")
.map((post) => post.title.trim());
chainが長くなり、途中結果やerror条件が見えにくいなら、名前付きの中間変数へ分けます。1回のreduceへ無理にまとめることが常に読みやすいとは限りません。
疎な配列では何が起きる?
map()、filter()、reduce()は、値が割り当てられていない空slotではcallbackを呼びません。疎な配列を通常のundefined配列と同じだと考えないようにします。
const sparse = [1, , 3];
console.log(sparse.map((value) => value * 2)); // [2, empty, 6]
console.log(sparse.filter(() => true)); // [1, 3]
データ処理では疎な配列を意図せず作らないことが第一です。長さから値を生成するならArray.from()の方法を使えます。
配列処理が失敗したときの確認手順
- 欲しい結果が「配列」「boolean」「1値」のどれかを決め直す
- callbackが各分岐で値をreturnしているか確認する
- object要素を意図せず変更していないか確認する
reduce()へ結果型に合う初期値を渡す- 空配列、疎な配列、nullish値を小さな入力でテストする
結果が必要なのに副作用だけのcallbackを書いている場合は、まず純粋な変換へ戻します。存在・全件の確認だけならsome・everyへ切り替えると目的が明確になります。