はじめに
Array
のmap
、reduce
、filter
などを使って配列をイイ感じに操作する方法をまとめました。
for
文を使うと「どうやるか」といったコードになりがちですが、map
とかfilter
とか使うと「何をするか」が宣言的に書けます。
(※for
文にも無限を扱えたり、yield
とか早いとかいいところはあると思います。)
もっとイケてる書き方があるという場合は教えてくれると嬉しいです。
詳しい構文はMDN のリンクを張ったのでそちらが参考になると思います。
前置き①②はアロー関数と高階関数です。
Array
のmap
やreduce
などは関数を引数とするので切り離せない存在です。
前置き①:アロー関数
以下の関数をアロー関数に変形します。
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
対象のデータ
ここからArray
のmap
やreduce
などの内容です。
サンプルデータとして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"]}
]
*/
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
です。
const product = list.find(p => p.id === 2);
console.log(product);
// {id: 2, name: "bar", active: false, price: 1000, categories: ["a", "c"]}
every
全ての要素が条件を満たすかを真偽値で返します。
const all1000YenOrLess = list.every(p => p.price <= 1000);
console.log(all1000YenOrLess);
// false
some
条件を満たす要素が含まれるかを真偽値で返します。
const exists = list.some(p => p.categories.includes("c"));
console.log(exists);
// true
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では0
はfalsy
な値とみなされるため、a.price - b.price
が0
の場合、||
によりa.id - b.id
が評価されます。
[...list]
とArray.of(...list)
は同じです。
まとめ
for
と違い、書き手の意図を読み取りやすいのがmap
とかfilter
のいいところだと思います。
ほとんどの場合、for
文よりスマートに書けますが、メソッドチェーン(list.map().filter()....
)するたびに中間配列が作られます。
メモリにシビアな環境ではfor
のほうが良いようです。
groupBy
やらもreduce
で頑張ればできますが、JavaScriptは配列操作に弱いので素直にライブラリ頼ったほうが良さそうです。
-
Lodash:https://lodash.com/
- おなじみの使いやすい配列操作ライブラリです。
-
lazy.js: http://danieltao.com/lazy.js/
- Lodashっぽいですが、遅延評価です。
-
linq.js: https://github.com/neuecc/linq.js/
- C# の言語統合クエリの js の実装。
- LINQ 同様に遅延評価です。
-
Rx.js: http://reactivex.io/rxjs/
- 今波が来てるリアクティブ・プログラミング用のライブラリです。
- 非同期処理をイイ感じ書けます。
- Rx.NETとかRxGoとかRxPYとかRx.rbとかRxSwiftとかreaxiveとかいろいろあります。