Posted at

Array.prototype.reduce()をガンガン使おう

JavaScriptでよく使うメソッドはなんですか?

僕は Array.prototype.reduce() だと思います。

reduce自体は遥か昔、ES5 からあります。ただreduceが本領を発揮し始めたのは ES2015 から

あたりが整ってからではないでしょうか。

そんなES2015も既に昔の話ですが、なんとなく、reduce使ってる人は使ってるし、使ってない人は使ってない(トートロジー)という印象があります。

そこで、今からreduce学ぶ人はどんな気持ちなんだろう?と軽く検索してみたんですが、どうにも解説がES5時代のものだったりしていまいち実践的な使い方がされてないように感じたので、ちょっとパターンをまとめてみることにしました。


sum

const items = [0, 1, 2]

items.reduce((acc, item) => acc + item, 0)
// -> 3

sumですね。Array内の要素を足してNumberに変換します。

これがreduceの基本と紹介されることが多いんじゃないかと思います。

しかしまあ、へ~そうなんだ~って感じでここ止まりになってしまう印象もあります。


Array.prototype.map風

const items = [0, 1, 2]

items.reduce((acc, item) => [...acc, item * 2], [])
// -> [0, 2, 4]
// ≒items.map(item => item * 2)

配列の各要素を変換するmapもreduceで書けます。

ここで出てくるのが分割代入とスプレッド構文です。... で開いて、[] で括って新しい配列を返します。

ただしこの例の場合はreduce使うのは冗長なので素直にmapを使ったほうが良いです。


Array.prototype.filter風

const items = [0, 1, 2]

const isEven = num => num % 2 === 0
items.reduce((acc, item) => (isEven(item) ? [...acc, item] : acc), [])
// -> [0, 2]
// ≒items.filter(item => isEven(item))

配列から特定の要素だけを抽出するfilterもreduceで書けます。

reduceの場合、filter条件にマッチしない場合は acc をそのまま返せばフィルタリングされることになります。

ただしこれも普通にfilter使ったほうが良いですね。


Objectをfilter

filterはArrayのメソッドなのでObjectに対しては使えません。けれど同様にObjectに対してもフィルタリングしたいケースもあります。そういうときにはreduceの出番です。

const obj = { a: 0, b: 1, c: 2 }

const isEven = num => num % 2 === 0

Object.keys(obj).reduce(
(acc, key) => (isEven(obj[key]) ? { ...acc, [key]: obj[key] } : acc),
{}
)
// -> {a: 0, c: 2}

Object.keysでkeyの配列にすることでreduceします。

いったんArrayにしますが最終的にはObjectを返すので末尾の初期値は{}になります。

新しく返すobjectはkey変数を [] で括ることによりComputed property namesとして設定します。


Object.entries()

Object.entries(obj).reduce(

(acc, [key, value]) => (isEven(value) ? { ...acc, [key]: value } : acc),
{}
)

ES2017からは [key, value] の配列にしてくれる Object.entries() というのもあります。こちらを使うとreduceのコールバック内でobjを参照せず引数のみで処理できるのでよりスマートになります。


keyによるフィルタ(非破壊delete)

valueではなくkey側のフィルタです。

要はdelete的な処理になりますが、非破壊で新しいObjectを返します。

const omit = (obj, omitKeys) =>

Object.entries(obj).reduce(
(acc, [key, value]) => (omitKeys.includes(key) ? acc : { ...acc, [key]: value }),
{}
)

const obj = { a: 0, b: 1, c: 2 }
const omitted = omit(obj, ['b', 'e'])
// obj -> {a: 1, b: 1, c: 3}
// omitted -> {a: 1, c: 3}

ここでは消したいkeyを配列で指定して新しいobjectを返す関数を定義しました。


deleteとの違い

const del = (obj, omitKeys) => omitKeys.forEach(key => delete obj[key])

const obj = { a: 0, b: 1, c: 2 }
del(obj, ['b', 'e'])
// obj -> {a: 1, c: 3}

deleteで処理する場合は引数であるobjectを破壊的に変更します。

元のobjectには変更を加えたくない場合にはこれだとうまくいきません。


Objectのkey名を変更する

const obj = { alpha: 0, bravo: 1, charlie: 2 }

Object.entries(obj).reduce((acc, [key, value]) => ({ ...acc, [key.toUpperCase()]: value }), {})
// -> {ALPHA: 0, BRAVO: 1, CHARLIE: 2}

valueではなくkeyをmapするような感じです。

keyに対してmap処理を施してComputed property namesとして設定します。

ここで一つ文法的に気をつけないといけないのは、今回は{}で括ったobjectだけをreturnしているので、ブロックで使われる{}と区別がつかなくなります。そのためobjectのみを返す場合はさらに()で括る必要があります。

Object.entries(obj).reduce((acc, [key, value]) => {

return { ...acc, [key.toUpperCase()]: value }
}, {})

ブロックとobjectの{}を両方使ってreturnを明示する場合だとこうなります。

どちらが好みかという問題はありますが、ブロックを作らないほうが返り値のみを書くぞという決意の表れがあります(可読性は?)。なので配列からオブジェクトを返そうと思ったら、

.reduce((acc, item) => ({...acc,}), {})

とりあえず無意識にここまで打ってるのではないでしょうか


Map風Objectを作る

const users = [

{ id: 'a', name: '佐藤' },
{ id: 'b', name: '鈴木' },
{ id: 'c', name: '高橋' }
]
const userMap = users.reduce((acc, user) => ({ ...acc, [user.id]: user }), {})
// {
// a: { id: 'a', name: '佐藤' },
// b: { id: 'b', name: '鈴木' },
// c: { id: 'c', name: '高橋' }
// }

userMap['b']
// -> { id: 'b', name: '鈴木' }

objectの配列からobjectにします。

毎回配列からfindすると大変なので、keyで取り出せるようにobjectに変換するケースで使います。


大量に捌く場合はスプレッドせず破壊的に

[...Array(10000).keys()].reduce((acc, item) => [...acc, item], [])

この処理は1万件の配列を回して特に手を加えず新しい配列にするだけです。

しかしスプレッド構文で追加していくと回す回数が増えるほど劇的に遅くなります。10万件にした場合、自分の環境では30秒以上かかりました。

[...Array(1000000).keys()].reduce((acc, item) => {

acc.push(item)
return acc
}, [])

pushしてreturnすれば100万件くらいでも大丈夫です。


要素ごとにカウントする

(

'あるアグリゲートを他のアグリゲートから区別するのは、そのアグリゲートに含まれるオブジェクトと含まれないオブジェクトの間にある境界線です。それぞれのアグリゲートはひとつのルートを持ちます。ルートになるのはエンティティで、アグリゲートの外部からアクセスできる唯一のオブジェクトです。ルートはアグリゲート内のオブジェクトの参照を保持します。アグリゲート内のオブジェクトは互いに参照を持つことができますが、アグリゲートの外部のオブジェクトはルートの参照しか保持できません。アグリゲートの内部にルート以外のエンティティがある場合、そのエンティティはそのアグリゲート内で一意であればいいでしょう。'
.match(/アグリゲート|エンティティ|オブジェクト|ルート/g) || []
).reduce((acc, key) => ({ ...acc, [key]: (acc[key] || 0) + 1 }), {})
// -> { アグリゲート: 10, オブジェクト: 6, ルート: 5, エンティティ: 3 }

ラストにオチですが、この記事を書こうと思ったきっかけです。

ゲシュタルト崩壊したアグリゲートもreduce一発で崩壊具合が分かるのでべんりですね。