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

map と filter — 配列を変換するか絞り込むか

配列の各要素を変換する map と、条件に合う要素だけを残す filter を扱います。元の配列を変えない性質、filter から map へつなぐチェーン、位置を受け取る index 引数まで確かめます。

メニューの一覧から税込価格だけを取り出したり、在庫のある商品だけを抜き出したりする場面では、空の配列を宣言してから forEach の中で push する行が毎回付いてきます。

この記事では、要素を変換する map と、条件に合う要素だけを残す filter を扱います。

1 件ずつ変換して新しい配列にする — map

カフェのメニューの税抜価格を、すべて税込に直した一覧が欲しいとします。元の 3 件と同じ並びのまま、価格だけが差し替わった配列を作る形です。1 件ずつ計算するところまでは forEach で書けますが、計算した値をまとめるには、結果を集める配列を自分で用意して最後まで管理することになります。

配列のメソッド map(各要素をコールバックに通し、その戻り値を並べた新しい配列を返すメソッド)は、結果を集める配列の用意と繰り返しの両方を引き受けます。渡す関数には 1 件分の変換だけを書き、変換後の値を返します。

const menuItems = [
  { name: "ブレンドコーヒー", price: 480 },
  { name: "カフェラテ", price: 540 },
  { name: "チーズケーキ", price: 620 },
];

// 1 件分の変換だけを関数にする
const toTaxIncluded = (item) => Math.floor(item.price * 1.1);

// 要素の数だけ toTaxIncluded が呼ばれ、戻り値が新しい配列に並ぶ
const taxIncluded = menuItems.map(toTaxIncluded);
console.log(taxIncluded.join(", "));   // 528, 594, 682
console.log(taxIncluded.length);       // 3
console.log(menuItems.length);         // 3
返した値が同じ位置に並ぶ
toTaxIncluded は1 つだけmenuItems の0 番目 480menuItems の1 番目 540menuItems の2 番目 620480 × 1.1 →528 を返す540 × 1.1 →594 を返す620 × 1.1 →682 を返すtaxIncluded の0 番目taxIncluded の1 番目taxIncluded の2 番目
元の要素と同じ順で、返した 3 つの値が taxIncluded に入ります。map は件数を変えず、値だけを差し替えます

map は要素を 1 件ずつコールバックへ渡し、返ってきた値をその都度新しい配列へ積むだけなので、件数が動く余地がありません。件数を変えたい絞り込みには、別のメソッドが要ります。

forEach の戻り値は受け取れません

forEach は渡した関数を呼ぶだけで、戻り値を集めません。返るのは常に undefined なので、const list = menuItems.forEach(toTaxIncluded); と書いても listundefined です。結果を配列として受け取るには map を使います。

パン屋の商品を、セール価格と商品名の一覧にします。bakeryItems は宣言済みです。

① セール価格(price の 0.8 倍を切り捨て)を並べた配列 salePrices を作ってください。

② salePrices を「, 」でつないで表示してください。

③ 商品名だけの配列を作り、「 / 」でつないで表示してください。

④ bakeryItems の 1 件目の price を表示してください。

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

JavaScript / TypeScript エディタ

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

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

在庫のある本だけを一覧に出したいとします。map は件数を変えないため在庫切れを外す用途には使えず、forEach の中で if を書いて push する形に戻ってしまいます。

filter(コールバックが true を返した要素だけを集めた新しい配列を返すメソッド)は、この絞り込みを引き受けます。渡す関数は要素を受け取り、残すかどうかを真偽値で返します。

const books = [
  { title: "実践 SQL 入門", stock: 0 },
  { title: "はじめてのネットワーク", stock: 5 },
  { title: "図解 Linux 入門", stock: 2 },
];

// 残すかどうかを真偽値で返す関数にする
const isInStock = (book) => book.stock > 0;

// true を返した要素だけが新しい配列に入る
const available = books.filter(isInStock);
console.log(available.length);      // 2
console.log(available[0].title);    // はじめてのネットワーク
console.log(books.length);          // 3
残った要素は前に詰め直される
books の 0 番目在庫 0stock > 0 がfalseavailable に入らないbooks の 1 番目在庫 5stock > 0 がtrueavailable の0 番目books の 2 番目在庫 2stock > 0 がtrueavailable の1 番目
true を返した 2 件だけが available に入ります。外れた要素の分だけ件数が減り、残った要素は前に詰めて並びます

available から読めるのは詰め直したあとの位置だけで、元の books で何番目だったかは残りません。元の位置を使う方法は、この記事の最後で扱います。下の表は、渡した関数の戻り値が 3 つのメソッドでどう使われるかをまとめたものです。

メソッドコールバックの戻り値できる配列の件数
mapその値が新しい配列に入る元と同じ件数
filtertrue なら要素を残す判定に使う元と同じか、それより少ない
forEach使われず捨てられる配列は作られない(undefined が返る)

波括弧を書いたら return が要ります

(book) => { book.stock > 0 } のように波括弧で本体を書くと、return を書かない限り戻り値は undefined です。undefined は falsy なので、どの要素も残らず空の配列になります。1 行で判定するなら波括弧を付けずに書きます。

観葉植物の通販ページで、鉢のサイズごとに一覧を分けます。plants は宣言済みです。

① 大鉢の商品だけを残した配列 largePots を作り、その件数を表示してください。

② largePots に残った商品名を「, 」でつないで表示してください。

③ 小鉢の商品だけを残した配列 smallPots を作り、商品名を「, 」でつないで表示してください。

④ plants の件数を表示してください。

JavaScript / TypeScript エディタ

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

絞ってから変換する — filter と map をつなぐ

公開中の講座だけを、画面に出す文字列に直したいとします。filter の結果を変数に入れてから、その変数に .map(...) を呼ぶ 2 行でも書けますが、あとで使わない途中の配列にも名前を付けることになります。

filter が返すのも配列なので、その戻り値に続けて .map(...) を書けます。メソッドの呼び出しを続けて書くこの形を チェーン(前のメソッドの戻り値に対して次のメソッドを呼ぶ書き方)と呼びます。

const courses = [
  { title: "はじめての Git", minutes: 45, published: true },
  { title: "SQL 実践演習", minutes: 90, published: false },
  { title: "Linux 入門", minutes: 60, published: true },
];

// 公開中だけを残し、その結果を表示用の文字列に変換する
// 行頭の . は前の行の戻り値に続けて呼ぶ(改行しても 1 つの式)
const labels = courses
  .filter((course) => course.published)   // published はすでに真偽値なので、比較を書かずに返す
  .map((course) => `${course.title}${course.minutes}分)`);

console.log(labels.join(" / "));
// はじめての Git(45分) / Linux 入門(60分)
console.log(labels.length);    // 2
console.log(courses.length);   // 3
チェーンで順にたどる 3 つの配列
courses3 件のオブジェクト元の配列は3 件のまま.filter(...)2 件のオブジェクト名前は付いていない.map(...)2 件の文字列labels に入る
filter が返した配列は名前を持たないまま .map に読まれます。つなぐたびに新しい配列が 1 本ずつ増えます

途中の値を確かめたいときは、filter の結果をいったん変数に入れて console.log で表示してから、その変数に .map(...) を呼びます。確かめ終えたら、チェーンの形に戻せます。

補講の 5 点を加えて、合格ラインの 75 点と比べます。records は宣言済みです。

① 全員の点数に 5 点を足し、続けて 75 点以上だけを残した配列 passedScores を、チェーンで作ってください。

② passedScores を「, 」でつないで表示してください。

③ records の 2 件目の score を表示してください。

④ 加点後に 75 点以上になる人の氏名をチェーンで作り、「 / 」でつないで表示してください。

JavaScript / TypeScript エディタ

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

何番目かを受け取る — コールバックの index

ランキングの一覧に「1 位」「2 位」と順位を添えたいとします。要素そのものには順位が入っていないため、数える変数を外に置いて、コールバックの中で 1 ずつ増やす形になりがちです。

mapfilter は、コールバックの 1 つ目の引数に要素を、2 つ目の引数にその要素の位置(0 から数えた index)を渡します。引数は前から順に渡るため、位置だけを使いたいときも、1 つ目の引数は書いておきます。この位置を index という名前で受け取れば、順位の表示にも、先頭から何件目までを残すかの判定にも使えます。

const ranking = ["ワイヤレスイヤホン", "モバイルバッテリー", "スマホスタンド", "USB ハブ"];

// 2 つ目の引数に 0 から数えた位置が渡る
const lines = ranking.map((item, index) => `${index + 1} 位: ${item}`);
console.log(lines.join("\n"));
// 1 位: ワイヤレスイヤホン
// 2 位: モバイルバッテリー
// 3 位: スマホスタンド
// 4 位: USB ハブ

// filter のコールバックも同じ順番で受け取る
const topThree = ranking.filter((item, index) => index < 3);
console.log(topThree.length);   // 3
同じ index の 2 通りの使い道
map に(item, index)4 件それぞれに0〜3 が渡るindex + 1 を文字列に混ぜる4 件のまま順位が付くfilter に(item, index)map と同じ0〜3 が渡るindex < 3 で残すか決める3 件に減る値は元のまま
どちらのコールバックも、2 つ目の引数に 0 から数えた位置を受け取ります。map は位置を値に混ぜ、filter は位置で残すかどうかを決めます

filter に渡る index は、絞り込む前の配列での位置です。残った要素があとで前に詰められても判定には影響しないため、index < 3 は元の先頭 3 件を指します。

特集ページには、記事がおすすめ順に並んでいます。articles と viewCounts は宣言済みで、viewCounts は同じ順に並んだ閲覧数です。

① viewCounts を使い、「タイトル(◯ 回)」の形の配列を作って 1 行ずつ表示してください。

② 閲覧数が 5000 回以上の記事だけを残し、「, 」でつないで表示してください。

③ 2 位・4 位のように偶数位の記事だけを残し、「, 」でつないで表示してください。

JavaScript / TypeScript エディタ

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

理解度チェック

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

Q1prices.map((price) => price * 2) を実行したあと、元の prices はどうなりますか?

Q2courses.filter(isPublished).map(toLabel) で、map が読むのはどの配列ですか?

Q3ranking.map((item, index) => ...) のコールバックで、index に渡るのは何ですか?