コハム

Webクリエイターのコハムが提供する、Web制作に役立つ情報をまとめたブログです。

JavaScriptの配列メソッド完全解説:map・filter・reduce再入門

JavaScript map, filter and reduce functions explained, with examples

記事は上記記事を意訳したものです。

※当ブログでの翻訳記事は元サイト様に許可を得て掲載しています。


この記事では、JavaScriptコードでmap、filter、reduceをいつ、なぜ使うべきかを例を交えて説明します。これらのメソッドがコードをより宣言的で読みやすくする方法と、関数型プログラミングの考え方への入り口となる方法を紹介します。

まず、forループとfor...ofループを使用した単純なアプリケーション例から始めます。その後、同じ例をfilterとmapを使用して書き直し、さらにreduceを使用して再度書き直します。これらの解決策の可読性とパフォーマンスについて議論し、for...ofを使用した方が良い場合についても触れます。

最後に、これら3つの配列メソッドの中で最も複雑なreduceの使用例を見ていきます。

サンプルアプリケーション:アインシュタインの名言

非常にシンプルなサンプルアプリケーションを作ります。APIエンドポイントから有名な引用を取得し、アルバート・アインシュタインの引用を抽出し、DOMにレンダリングするウェブページです。

データは以下のような構造です:

[
  {
    quoteText: 'I never think of the future. It comes soon enough.',
    quoteAuthor: 'Albert Einstein',
  },
  {
    quoteText: 'In the middle of every difficulty lies opportunity.',
    quoteAuthor: 'Albert Einstein',
  },
  {
    quoteText: 'Courage is going from failure to failure without losing enthusiasm.',
    quoteAuthor: 'Winston Churchill'
  }
]

この関数は、アインシュタインの引用のquoteTextだけを含む配列を返すことを目的としています:

[
  'I never think of the future. It comes soon enough.',
  'In the middle of every difficulty lies opportunity.',
]

配列を反復処理する際、多くの開発者はすぐにfor、forEach、またはfor...ofを使用します。これらの構文にも用途はありますが、多くの場合、命令的で冗長で、読みにくく理解しづらいコードになってしまいます。

最初の試み:forループ

まず、forループを使用してこの問題を解決する方法を見てみましょう:

/**
 * forループでデータを処理し、必要な項目のみを配列にpushするために
 * if文を使用します。
 */
function getEinsteinQuotes(quotes) {
  const einsteinQuotes = [];

  for (let i = 0; i < quotes.length; i++) {
    const quote = quotes[i];
    if (quote.quoteAuthor.includes('Einstein')) {
      einsteinQuotes.push(quote.quoteText);
    }
  }

  return einsteinQuotes;
}

このコードは確かに機能しますが、問題があります:

  • 非常に読みにくい
  • 不必要なコードが多い
  • 実装時やメンテナンス時により多くのミスやバグを引き起こす可能性がある

特に、多くのロジックが配列操作の低レベルな詳細に関係しています:

  • let i = 0; i < quotes.length; i++ は配列の反復処理の制御フローを指定しています
  • const einsteinQuotes = [];einsteinQuotes.push(quote.quoteText) は配列間のアイテム移動の解決策です

これらは関数の目的である「ビジネスロジック」とは明確な関係がありません。

2番目の試み:for...ofループ

次に、for...ofループを使用して同じ問題を解決する方法を見てみましょう:

/**
 * for...ofループでデータを処理し、必要な項目のみを配列にpushするために
 * if文を使用します。
 */
function getEinsteinQuotesList(quotes) {
  const einsteinQuotes = [];

  for (let quote of quotes) {
    if (quote.quoteAuthor.includes('Einstein')) {
      einsteinQuotes.push(quote.quoteText);
    }
  }

  return einsteinQuotes;
}

これはforループの方法よりは少し良くなっていますが、まだ完璧ではありません。

for...ofループは配列の各項目を順番に反復処理します。forループとは異なり、ループの制御フローのロジックを手動で書く必要はありません。

3番目の試み:filterとmap

次に、私が最良の解決策と考えるfilterとmapを使用した方法を見てみましょう:

/**
 * filterとmapを使用してquotesデータを変換します。
 */
function getEinsteinQuotesList(quotes) {
  return quotes
    .filter(q => q.quoteAuthor.includes("Einstein"))
    .map(q => q.quoteText);
}

ここでは最初の2つの試みとは全く異なるアプローチを取ります。

  • filterは配列の各項目に対して述語関数(trueまたはfalseを返す関数)を実行し、フィルタリングする項目を決定します
  • mapはフィルタリングされた配列の各項目に対してコールバック関数を実行し、新しい値を返します

このアプローチの利点は主に可読性と簡潔さにあります:

  • 空の配列を明示的に作成して追加する必要がない
  • filterとmapは非常に読みやすく宣言的
  • データに対して実行している操作がメソッド名ですぐに分かる

ボーナス:reduce

最後に、reduceを使用して同じ関数を実装する方法を見てみましょう:

/**
 * reduceを使用してquotesデータを変換します。
 */
function getEinsteinQuotesList(quotes) {
  return quotes.reduce(
    (accumulator, q) => q.quoteAuthor.includes("Einstein") 
      ? [ ...accumulator, q.quoteText ]
      : accumulator, 
    []
  );
}

これは理解するのに少し時間がかかるため、一般的にはfilterとmapを最適な解決策として推奨します(パフォーマンスを除いて)。

パフォーマンス

  • forループとfor...ofループ(およびforEach関数)は最新のJSエンジンでは性能的に同等です
  • map、filter、reduceは一般的にforとfor...ofループよりも少し遅い
  • 大規模なデータセットで作業する場合は特に顕著です

3番目の試みでは、mapとfilterを連鎖させたことで: - 各配列要素を2回反復処理 - 新しい配列を2回作成

しかし、小規模または中規模のデータセットで作業している場合は、可読性とメンテナンス性の高いコードを書き、後でリファクタリングする方が良いでしょう。

forまたはfor...ofを使用すべき場合は?

はい、for...ofには多くの良い使用例があります(個人的にはforよりもfor...ofを常に使用することをお勧めします)。

  1. パフォーマンス面で優れている場合がある
  2. DOM APIの呼び出しなど、副作用の実行に使用すべき

例えば:

function appendQuotesToDOM(quotes) {
  const ul = document.getElementById("quotes");
  const fragment = document.createDocumentFragment();

  for (let quote of quotes) {
    const el = document.createElement("li");
    el.appendChild(document.createTextNode(quote));
    fragment.appendChild(el);
  };

  ul.appendChild(fragment);
}

reduceのさらなる例

配列の合計

const sumArray = (arr) => arr.reduce((acc, n) => acc + n);

const sum = sumArray([1,2,3,4,5]);
console.log(sum); // 15

配列を辞書に変換

const createDictionary = (arr, key) => {
  return arr.reduce((dict, obj) => {
    dict[obj[key]] = obj;
    return dict;
  }, {});
}

const myArray = [
  { id: '174d0a85', name: 'Jimmy' },
  { id: '9b7e77e2', name: 'Sarah' },
]

const myDict = createDictionary(myArray, 'id');

pipeとcompose関数の実装

const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

const subtract1 = x => x - 1;
const square = x => x * x;
const add1 = x => x + 1;

const subtract1ThenSquareThenAdd1 = pipe(subtract1, square, add1);
const result = subtract1ThenSquareThenAdd1(6);
console.log(result); // 26

プロミスの順次解決

const urls = [
  'https://some-api.com/endpoint1',
  'https://some-api.com/endpoint2',
  'https://some-api.com/endpoint3',
];

urls.reduce(async (previousPromise, url) => {
  await previousPromise;
  return fetch(url);
}, Promise.resolve());

結論

この記事では、map、filter、reduceの使用方法と使用タイミングの例を見てきました。これらの関数の動作についてより深い理解が得られたことを願っています。

これら3つのメソッドを理解したら、some、every、includes、findなどの他の便利な配列メソッドを学ぶことができます。また、より高度な配列操作にはlodashライブラリや、より関数型のアプローチを取るramdaなどのライブラリがあります。

©コハム