Q1prices.map((price) => price * 2) を実行したあと、元の prices はどうなりますか?
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
taxIncluded に入ります。map は件数を変えず、値だけを差し替えます。map は要素を 1 件ずつコールバックへ渡し、返ってきた値をその都度新しい配列へ積むだけなので、件数が動く余地がありません。件数を変えたい絞り込みには、別のメソッドが要ります。
forEach の戻り値は受け取れません
forEach は渡した関数を呼ぶだけで、戻り値を集めません。返るのは常に undefined なので、const list = menuItems.forEach(toTaxIncluded); と書いても list は undefined です。結果を配列として受け取るには map を使います。
条件に合う要素だけを残す — 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
true を返した 2 件だけが available に入ります。外れた要素の分だけ件数が減り、残った要素は前に詰めて並びます。available から読めるのは詰め直したあとの位置だけで、元の books で何番目だったかは残りません。元の位置を使う方法は、この記事の最後で扱います。下の表は、渡した関数の戻り値が 3 つのメソッドでどう使われるかをまとめたものです。
| メソッド | コールバックの戻り値 | できる配列の件数 |
|---|---|---|
| map | その値が新しい配列に入る | 元と同じ件数 |
| filter | true なら要素を残す判定に使う | 元と同じか、それより少ない |
| forEach | 使われず捨てられる | 配列は作られない(undefined が返る) |
波括弧を書いたら return が要ります
(book) => { book.stock > 0 } のように波括弧で本体を書くと、return を書かない限り戻り値は undefined です。undefined は falsy なので、どの要素も残らず空の配列になります。1 行で判定するなら波括弧を付けずに書きます。
絞ってから変換する — 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
filter が返した配列は名前を持たないまま .map に読まれます。つなぐたびに新しい配列が 1 本ずつ増えます。途中の値を確かめたいときは、filter の結果をいったん変数に入れて console.log で表示してから、その変数に .map(...) を呼びます。確かめ終えたら、チェーンの形に戻せます。
何番目かを受け取る — コールバックの index
ランキングの一覧に「1 位」「2 位」と順位を添えたいとします。要素そのものには順位が入っていないため、数える変数を外に置いて、コールバックの中で 1 ずつ増やす形になりがちです。
map と filter は、コールバックの 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
filter に渡る index は、絞り込む前の配列での位置です。残った要素があとで前に詰められても判定には影響しないため、index < 3 は元の先頭 3 件を指します。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2courses.filter(isPublished).map(toLabel) で、map が読むのはどの配列ですか?
Q3ranking.map((item, index) => ...) のコールバックで、index に渡るのは何ですか?