37
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【JavaScript】イイ感じに配列を扱うイディオムまとめ。map filter reduce...

Last updated at Posted at 2018-04-22

はじめに

Arraymapreducefilterなどを使って配列をイイ感じに操作する方法をまとめました。
for文を使うと「どうやるか」といったコードになりがちですが、mapとかfilterとか使うと「何をするか」が宣言的に書けます。
(※for文にも無限を扱えたり、yieldとか早いとかいいところはあると思います。)

もっとイケてる書き方があるという場合は教えてくれると嬉しいです。

詳しい構文はMDN のリンクを張ったのでそちらが参考になると思います。
前置き①②はアロー関数と高階関数です。
Arraymapreduceなどは関数を引数とするので切り離せない存在です。

前置き①:アロー関数

以下の関数をアロー関数に変形します。

二乗を返す関数
function square(num) {
  return num ** 2;
}

変数に匿名関数を代入しても同じです。

const square = function(num) {
  return num ** 2;
};

アロー関数ではfunctionが消え、=>で引数と関数本体をつなぎます。

const square = (num) => {
  return num ** 2;
};

アロー関数は引数が一つだけの場合、() を省略できます。

const square = num => {
  return num ** 2;
};

関数本体が1文の場合、{} を省略でます。
その場合、関数本体が戻り値になるのでreturnも不要です。

const square = num => num ** 2;

前置き②:高階関数

JavaScript の関数は第一級オブジェクトであるため、関数を引数や戻り値にできます。

配列の条件に一致する要素の数を返す関数
/**
 * 配列の条件に一致する要素の数を返す
 * @param {Array} list - 配列
 * @param {function} judgeFunc - trueまたはfalseを返す判定関数
 */
function count(list = [], judgeFunc) {
  let num = 0;
  for (const item of list) {
    if (judgeFunc(item)) {
      num++;
    }
  }
  return num;
}

// ランダムな数値の入った配列
const myList = [21, 48, 18, 1, 18, 37, 39, 2, 40, 14, 36, 6, 43, 17, 45, 34, 38, 21, 30, 12];

// 7で割り切れるかどうか
function myJudge1(num) {
  return num % 7 === 0;
}

// 3で割り切れるかどうか
function myJudge2(num) {
  return num % 3 === 0;
}

console.log(count(myList, myJudge1));  // 3
// ※関数そのものを渡しているので myJudge に () は付かない

console.log(count(myList, myJudge2));  // 11

関数を引数とするので柔軟に処理を切り替えられます。
上記のコードでは渡す関数を変えれば、素数を数えたり、5以下を数えたりできます。

引数に渡す関数を一回一回定義するのは冗長ですので、、countの呼び出し時、匿名関数として直接渡してみます。

const result = count(myList, function(num) {
  return num % 7 == 0;
});

console.log(result);  // 3

匿名関数はアロー関数にすると簡潔です。

const result = count(myList, num => num % 7 === 0);

console.log(result);  // 3

対象のデータ

ここからArraymapreduceなどの内容です。
サンプルデータとしてWeb API などから以下の製品一覧データをフェッチしたとします。

const list = [
  {
    id: 1,
    name: "foo",
    active: true,
    price: 500,
    categories: ["a", "b", "c"]
  },
  {
    id: 2,
    name: "bar",
    active: false,
    price: 1000,
    categories: ["a", "e"]
  },
  {
    id: 3,
    name: "baz",
    active: true,
    price: 1500,
    categories: ["c", "e"]
  }
];

map

対象の配列からあたらしい配列を作ります。

名前だけの配列を作る
const names = list.map(p => p.name);

console.log(names);
/*
  ["foo", "bar", "baz"]
*/
オブジェクトの配列を作る
const items = list.map(p => ({
  name: p.name,
  taxIncludePrice: p.price * 1.08
}));

console.log(items);
/*
  [
    {name: "foo", taxIncludePrice: 540},
    {name: "bar", taxIncludePrice: 1080},
    {name: "baz", taxIncludePrice: 1620}
  ]
*/

mapに限りませんがアロー関数で匿名オブジェクトを返す場合、{}()で囲みます。
()が無いと{}がアロー関数本体の括弧と見做されてしまいます。

最大値を求める
const maxPrice = Math.max(...list.map(p => p.price));

console.log(maxPrice);
// 1500

list.map() で作られた価格の配列を...(スプレッド構文)で展開すると、Math.maxにより最大値が得られます。

filter

配列から条件に一致した要素のみを抽出し新しく配列を作ります。

有効製品のみ抽出する
const activeProducts = list.filter(p => p.active);

console.log(activeProducts);
/*
  [
    {id: 1, name: "foo", active: true, price: 500, categories: ["a", "e"]},
    {id: 3, name: "baz", active: true, price: 1500, categories: ["c", "e"]}
  ]
*/
1000円を超える製品を抽出する
const over1000YenProducts = list.filter(p => p.price >= 1000);

console.log(over1000YenProducts);
/*
  [
    {id: 2, name: "bar", active: false, price: 1000, categories: ["a", "e"]},
    {id: 3, name: "baz", active: true, price: 1500, categories: ["c", "e"]}
  ]
*/

find

条件に一致した要素を一つ取り出します。
複数ある場合は最初にマッチした要素を返します。
見つからない場合、戻り値はundefindです。

idが2の製品を取得する
const product = list.find(p => p.id === 2);

console.log(product);
// {id: 2, name: "bar", active: false, price: 1000, categories: ["a", "c"]}

every

全ての要素が条件を満たすかを真偽値で返します。

すべての製品の価格が1000円以下か?
const all1000YenOrLess = list.every(p => p.price <= 1000);

console.log(all1000YenOrLess);
// false

some

条件を満たす要素が含まれるかを真偽値で返します。

カテゴリcを含む製品が存在するか?
const exists = list.some(p => p.categories.includes("c"));

console.log(exists);
// true
税込み1600円以上の製品が存在するか
const exists = list.some(p => p.price * 1.08 >= 1600);

console.log(exists)
// ture

reduce

配列を一つの値に畳み込みます。
動きが分かりずらいのでデバッグでステップ実行すると理解しやすいです。
リンク先のHow reduce() worksが分かりやすいです。

合計金額を求める
const summary = list
  .map(p => p.price)
  .reduce((acc, cur) => acc + cur);

console.log(summary);
// 3000

// 平均金額を求める
const average = summary / list.length;
console.log(average);
// 1000
配列の入れ子を平坦化する
const categories = list
  .map(p => p.categories)
  .reduce((acc, cur) => [...acc, ...cur]);

console.log(categories);
// ["a", "b", "c", "a", "c", "a", "e"]

// 重複を除く
const distinct= list
  .map(p => p.categories)
  .reduce((acc, cur) => [...acc, ...cur])
  .filter((c, i, self) => self.indexOf(c) === i);
// ["a", "b", "c", "e"]

sort

値段で昇順ソート
const sorted = [...list].sort((a, b) => a.price - b.price);

console.log(sorted);
/*
  [
    {id: 1, name: "foo", active: true, price: 500, categories: ["a", "b", "c"]},
    {id: 2, name: "bar", active: false, price: 1000, categories: ["a", "c"]}
    {id: 3, name: "baz", active: true, price: 1500, categories: ["a", "e"]}
*/

[...list]で配列をコピーしています。(要素の参照は保たれます)
list.sortすると元の配列がソートされてしまうため注意が必要です。
降順ソートしたい場合はb.price - a.priceにします。

複数キーでソートする
const multipleSorted = Array.of(...list).sort(
  (a, b) => a.price - b.price || a.id - b.id
);

JavaScriptでは0falsyな値とみなされるため、a.price - b.price0の場合、||によりa.id - b.id が評価されます。
[...list]Array.of(...list)は同じです。

まとめ

forと違い、書き手の意図を読み取りやすいのがmapとかfilterのいいところだと思います。
ほとんどの場合、for文よりスマートに書けますが、メソッドチェーン(list.map().filter()....)するたびに中間配列が作られます。
メモリにシビアな環境ではforのほうが良いようです。

groupByやらもreduceで頑張ればできますが、JavaScriptは配列操作に弱いので素直にライブラリ頼ったほうが良さそうです。

  • Lodashhttps://lodash.com/
    • おなじみの使いやすい配列操作ライブラリです。
  • lazy.jshttp://danieltao.com/lazy.js/
    • Lodashっぽいですが、遅延評価です。
  • linq.jshttps://github.com/neuecc/linq.js/
    • C# の言語統合クエリの js の実装。
    • LINQ 同様に遅延評価です。
  • Rx.jshttp://reactivex.io/rxjs/
    • 今波が来てるリアクティブ・プログラミング用のライブラリです。
    • 非同期処理をイイ感じ書けます。
    • Rx.NETとかRxGoとかRxPYとかRx.rbとかRxSwiftとかreaxiveとかいろいろあります。
37
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?