本文へスキップ
ウェブエンジニア問題集
配列メソッド 1

配列の変換メソッド — map・filter・reduce・forEach

約16分
この章の目次開く

map・filter・reduce は、配列メソッドの中でも特に使用頻度が高い3つです。 各要素に対して「何かするだけ」なら、そこに forEach が加わります。 それぞれの役割は、次のように覚えると整理しやすくなります。

メソッド役割戻り値
map各要素を変換する新しい配列
filter条件に合う要素だけ残す新しい配列
reduce配列全体を1つの値にまとめる任意の値
forEach各要素に処理を実行するundefined

map — 各要素を変換して新しい配列を作る

mapが変換結果を新しい配列として返すイメージ

map は配列の各要素にコールバック関数を適用し、その戻り値で構成された新しい配列を返します。 元の配列は変更されません(非破壊)。

構文: array.map(callbackFn)

引数渡せるもの説明
callbackFn(element, index, array) => 値各要素を変換する関数。その戻り値がそのまま要素になる

戻り値: 変換結果からなる新しい配列。長さは必ず元の配列と同じ

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((n) => n * 2);
 
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(numbers); // [1, 2, 3, 4, 5](元の配列はそのまま)
js

コールバックの引数

const result = ['a', 'b', 'c'].map((element, index, array) => {
  // element: 現在の要素("a", "b", "c")
  // index:   現在のインデックス(0, 1, 2)
  // array:   元の配列自体
  return `${index}: ${element}`;
});
// ["0: a", "1: b", "2: c"]
js

map は要素数を変えられない

map の戻り値の長さは、呼び出した時点の元配列の長さと必ず一致します。コールバックが何を返しても、要素が増えたり減ったりすることはありません。

[1, 2, 3].map((n) => []); // [[], [], []] ← 長さは3のまま
[1, 2, 3].map((n) => undefined); // [undefined, undefined, undefined]
js

map には「この要素はスキップする」という書き方がありません。return を書かなかった場合も要素が消えるわけではなく、undefined が入るだけです。

// ❌ 1より大きいものだけ残すつもりだが、除けていない
const result = [1, 2, 3].map((n) => {
  if (n > 1) return n;
});
// [undefined, 2, 3] ← 長さは3のまま
js

要素数を変えたいときは、目的に応じてメソッドを変えます。

やりたいこと使うメソッド
要素を減らしたい(絞り込み)filter
絞り込んだうえで変換もしたいfilter + map のチェーン
1つの要素から複数の要素を作りたいflatMap
配列以外の形にまとめたいreduce

実務パターン: APIレスポンスの整形

const users = [
  { id: 1, first_name: '太郎', last_name: '田中' },
  { id: 2, first_name: '花子', last_name: '佐藤' },
];
 
const displayUsers = users.map((user) => ({
  id: user.id,
  fullName: `${user.last_name} ${user.first_name}`,
}));
// [{ id: 1, fullName: "田中 太郎" }, { id: 2, fullName: "佐藤 花子" }]
js

map を使うべきでない場面

map は「変換した新しい配列が欲しいとき」に使います。 戻り値を使わずに副作用(ログ出力、DOM操作など)だけが目的なら forEach を使ってください。

// ❌ mapの戻り値を捨てている
users.map((user) => {
  console.log(user.name);
});
 
// ⭕
users.forEach((user) => {
  console.log(user.name);
});
js

forEach — 各要素に対して処理を実行する

配列の各要素に対して処理を実行するイメージ

forEach は配列の各要素に対してコールバックを1回ずつ実行します。 map と違い、新しい配列は返しません。戻り値は常に undefined です。 ログ出力、通知の送信、既存オブジェクトへの書き込みなど、副作用そのものが目的のときに使います。

構文: array.forEach(callback)

引数説明
callback各要素に対して実行する関数。(element, index, array) を受け取れる

基本的な使い方

const fruits = ['りんご', 'みかん', 'ぶどう'];
 
fruits.forEach((fruit) => {
  console.log(fruit);
});
// りんご
// みかん
// ぶどう
js

インデックスが必要なときは第2引数を使います。

fruits.forEach((fruit, index) => {
  console.log(`${index + 1}番目: ${fruit}`);
});
// 1番目: りんご
// 2番目: みかん
// 3番目: ぶどう
js

実務パターン: 各要素に副作用を実行する

新しい配列を作るのではなく、「1件ずつ何かする」処理に向いています。

const users = [
  { name: '太郎', email: 'taro@example.com' },
  { name: '花子', email: 'hanako@example.com' },
];
 
users.forEach((user) => {
  sendWelcomeMail(user.email);
});
js

オブジェクトのプロパティを集計用の箱に足していく、といった書き方もできます。ただし集約が目的なら reduce の方が意図が明確です。

const counts = {};
 
['apple', 'banana', 'apple'].forEach((fruit) => {
  counts[fruit] = (counts[fruit] ?? 0) + 1;
});
 
console.log(counts); // { apple: 2, banana: 1 }
js

map との違い

どちらも「各要素を順に見る」点は同じですが、戻り値と目的が違います。

比較forEachmap
目的副作用を実行する新しい配列を作る
戻り値undefined新しい配列
メソッドチェーンできないできる
const numbers = [1, 2, 3];
 
const mapped = numbers.map((n) => n * 2);
console.log(mapped); // [2, 4, 6]
 
const fromForEach = numbers.forEach((n) => n * 2);
console.log(fromForEach); // undefined
js
学習者学習者

じゃあ、変換したいときは map、ログを出したいときは forEach って覚えればいい?

先生先生

その理解で十分。戻り値を使うなら map、使わないなら forEach。戻り値を捨てる map は、読んだ人が「変換結果はどこ?」と迷う原因になるよ。

途中で止められない

forEach のコールバックで return しても、その要素の処理を抜けるだけで、残りの要素は実行されます。break は使えません。

[1, 2, 3, 4].forEach((n) => {
  if (n === 3) return; // 3 だけスキップ。ループは続く
  console.log(n);
});
// 1
// 2
// 4
js

途中で止めたいときは for...of を使います。

for (const n of [1, 2, 3, 4]) {
  if (n === 3) break;
  console.log(n);
}
// 1
// 2
js
const ids = [1, 2, 3];
 
// ❌ ほぼ同時に走り、完了を待てない
ids.forEach(async (id) => {
  await fetch(`/api/users/${id}`);
});
 
// ⭕ 1件ずつ待つ
for (const id of ids) {
  await fetch(`/api/users/${id}`);
}
js

filter — 条件に合う要素だけを残す

filter はコールバックが true を返した要素だけで構成された新しい配列を返します。 元の配列は変更されません。

const numbers = [1, 2, 3, 4, 5, 6];
const evens = numbers.filter((n) => n % 2 === 0);
 
console.log(evens); // [2, 4, 6]
js

実務パターン: 検索フィルタリング

const products = [
  { name: 'ノートPC', price: 120000, inStock: true },
  { name: 'マウス', price: 3000, inStock: true },
  { name: 'キーボード', price: 15000, inStock: false },
  { name: 'モニター', price: 45000, inStock: true },
];
 
const result = products.filter((p) => p.inStock && p.price >= 10000);
// [{ name: "ノートPC", ... }, { name: "モニター", ... }]
js

filter + map のチェーン

filter で絞り込んでから map で変換する、というチェーンは実務で非常によく書きます。

const inStockNames = products.filter((p) => p.inStock).map((p) => p.name);
// ["ノートPC", "マウス", "モニター"]
js

該当なしの場合

条件に合う要素がなければ空配列 [] が返ります。null や undefined ではありません。

const result = [1, 2, 3].filter((n) => n > 10);
console.log(result); // []
console.log(result.length); // 0
js

reduce — 配列を1つの値にまとめる

配列全体を集計して1つの値にまとめるイメージ

map や filter が新しい配列を返すのに対し、reduce が返すのは配列ではなく1つの値です。

手元に集計用の箱を1つ用意して、配列の要素を先頭から順に箱へ入れていく作業だと考えてください。要素を1つ処理するたびに箱の中身を更新し、最後に残った箱の中身がそのまま reduce の戻り値になります。箱に入れるものは数値でもオブジェクトでも配列でもよいため、合計・グルーピング・別の形への変換まで、集計全般をこれ1つで書けます。

構文: array.reduce(callbackFn, initialValue)

reduce 自身の引数

reduce が受け取るのは、要素ごとに呼ばれる関数と、箱の初期値の2つです。

引数渡せるもの説明
callbackFn(第1引数)(accumulator, currentValue, currentIndex, array) => 値要素ごとに1回呼ばれる関数。その戻り値が次の accumulator になる
initialValue(第2引数・省略可)任意の値箱の初期値。省略した場合の挙動は後述(省略しないことを推奨)

戻り値: 最後の呼び出しで callbackFn が返した値。要素が1つもなく initialValue だけを渡した場合は、initialValue がそのまま返ります。

コールバックの引数

callbackFn は4つの引数を受け取れます。実務で書くのはほとんど先頭2つですが、残り2つもこの順番で必ず渡ってきます。

引数渡ってくる値説明
第1引数 accumulator途中経過の箱初回は initialValue、2回目以降は前回のコールバックの戻り値
第2引数 currentValue今見ている要素初期値ありなら先頭要素から、初期値なしなら2番目の要素から始まる
第3引数 currentIndexcurrentValue の位置初期値ありなら 0 から、初期値なしなら 1 から始まる
第4引数 arrayreduce を呼んだ配列自体走査中の元配列。「最後の要素かどうか」の判定などに使う

引数名は自由に決められます。実務では accumulator を acc、currentValue を cur や item と略すのが一般的です。

const joined = ['a', 'b', 'c'].reduce(
  (accumulator, currentValue, currentIndex, array) => {
    // accumulator:  前回の戻り値(初回は初期値の "")
    // currentValue: 現在の要素("a", "b", "c")
    // currentIndex: 現在のインデックス(0, 1, 2)
    // array:        元の配列自体(["a", "b", "c"])
    const isLast = currentIndex === array.length - 1;
    return accumulator + currentValue + (isLast ? '' : ', ');
  },
  '',
);
// "a, b, c"
js

基本的な使い方

もっとも単純な例が合計です。箱の初期値を 0 にして、要素を順に足していきます。

const numbers = [1, 2, 3];
const sum = numbers.reduce((accumulator, current) => {
  return accumulator + current;
}, 0);
// 6
js

このときコールバックは要素の数だけ、つまり3回呼ばれます。1回ごとの引数と戻り値は次のとおりです。

呼び出しaccumulatorcurrentValuecurrentIndex戻り値
1回目0(初期値)101
2回目1(1回目の戻り値)213
3回目3(2回目の戻り値)326

3回目の戻り値 6 が、そのまま reduce の戻り値になります。

コールバックの戻り値は「次の accumulator」になり、最後の戻り値が reduce 全体の返り値です。
学習者学習者

reduce だけ急に難しい…!accumulator って何者…?

先生先生

上の図の箱だよ。accumulator は途中経過を入れておく箱で、要素を1つ処理するたびに中身が更新される。 最後に残った箱が戻り値——合計なら数字、グルーピングならオブジェクトといった具合

ちなみに、accumulate は積み重ねる・蓄積するという意味です。

初期値は必ず指定する

初期値(第2引数)は省略できますが、省略すると挙動が次のように変わります。

  • accumulator の初回の値が配列の先頭要素になる
  • currentValue は2番目の要素から始まる
  • currentIndex は 0 ではなく 1 から始まる
  • コールバックの呼び出し回数が、要素数より1回少なくなる
// 初期値あり: コールバックは3回呼ばれる(index は 0, 1, 2)
[1, 2, 3].reduce((acc, cur) => acc + cur, 0); // 6
 
// 初期値なし: acc の初回は 1、コールバックは2回だけ呼ばれる(index は 1, 2)
[1, 2, 3].reduce((acc, cur) => acc + cur); // 6
js

合計のように「初期値と要素が同じ型」なら結果は変わりませんが、空配列に対して初期値なしで reduce を呼ぶと TypeError になります。初期値は常に指定するのが安全です。

// ❌ 空配列でエラー
[].reduce((acc, cur) => acc + cur);
// TypeError: Reduce of empty array with no initial value
 
// ⭕ 初期値を指定すれば空配列でも安全
[].reduce((acc, cur) => acc + cur, 0); // 0
js

配列の最大値を求める

合計以外にも、要素同士を比較して1つの値に絞り込む処理にも reduce が使えます。

const scores = [72, 95, 58, 88, 100];
const max = scores.reduce((acc, cur) => Math.max(acc, cur), -Infinity);
// 100
js

初期値に -Infinity を指定することで、どんな数値と比較しても最初の要素が必ず採用されます。

実務パターン: グルーピング

const orders = [
  { product: 'PC', category: 'electronics' },
  { product: 'シャツ', category: 'clothing' },
  { product: 'マウス', category: 'electronics' },
  { product: 'パンツ', category: 'clothing' },
];
 
const grouped = orders.reduce((acc, order) => {
  const key = order.category;
  if (!acc[key]) {
    acc[key] = [];
  }
  acc[key].push(order);
  return acc;
}, {});
js

なお、このグルーピングは ES2024 で追加された Object.groupBy でより簡潔に書けるようになりました。

const grouped = Object.groupBy(orders, (order) => order.category);
js

reduce を使いすぎない

reduce は汎用的すぎるがゆえに、何をしているか読みにくくなりがちです。 map + filter で書ける処理を無理に reduce で書く必要はありません。

// ❌ reduceで書いているが、やっていることはfilter + map
const result = numbers.reduce((acc, n) => {
  if (n > 3) acc.push(n * 2);
  return acc;
}, []);
 
// ⭕ 意図が明確
const result = numbers.filter((n) => n > 3).map((n) => n * 2);
js

メソッドチェーンの実践パターン

配列メソッドを連鎖させるメソッドチェーンは、実務で最もよく書くパターンの一つです。

const transactions = [
  { id: 1, type: 'income', amount: 50000, date: '2024-06' },
  { id: 2, type: 'expense', amount: 3000, date: '2024-06' },
  { id: 3, type: 'expense', amount: 15000, date: '2024-07' },
  { id: 4, type: 'income', amount: 80000, date: '2024-07' },
  { id: 5, type: 'expense', amount: 5000, date: '2024-06' },
];
 
const juneExpenses = transactions
  .filter((t) => t.type === 'expense')
  .filter((t) => t.date === '2024-06')
  .reduce((sum, t) => sum + t.amount, 0);
// 8000
js

チェーンは上から下に「絞り込み → 変換 → 集約」の順で読めるのが理想です。

途中で結果を確認したいとき

チェーンの途中で console.log を挟みたい場合、一時変数に切り出すと確認しやすくなります。

const expenses = transactions.filter((t) => t.type === 'expense');
console.log('expenses:', expenses);
const total = expenses.reduce((sum, t) => sum + t.amount, 0);
js

ちゃんと使うためのポイント

  • map は変換した新しい配列が欲しいときに使う
  • map の戻り値は必ず元と同じ長さ。要素を減らすなら filter、増減させるなら flatMap や reduce
  • コールバックで return を書き忘れても要素は消えず、undefined が入る
  • 副作用だけが目的なら forEach。戻り値を捨てる map は書かない
  • forEach は途中で止められない。break や順次の await が必要なら for...of
  • filter は条件に合う要素だけを残したいときに使う
  • reduce は合計、グルーピング、オブジェクトへの変換などの集約で使う
  • reduce で何でも書けるが、読みやすさでは filter + map の方がよいことも多い

次は、条件に合う要素を探したり判定したりする 配列の検索・判定メソッド を見ていきます。

参考リンク