const total = [0,1,2,3,4].reduce(function(previousValue, currentValue, index, array){
return previousValue + currentValue;
});
こういうサンプルをみて思うのが、「実際に1〜10の数字を足す場面なんてなくない?」ということ。たしかにreduceが何者なのかは理解できるのだが、実際に使われる場面がイメージできないのだ。
そんな感じでずっと腑に落ちないでいたのだが、HackerNoonの記事(Reduce your fears about Array.reduce())を読んで完全に理解できた!
ということで、自分でも理解を深めるためにreduceの使い方についてまとめる。
Array.prototype.reduce とは?
隣り合う 2 つの配列要素に対して(左から右へ)同時に関数を適用し、単一の値にします。
Array.prototype.reduce() - JavaScript | MDN
引数は以下のとおり。
- callback
- previousValue: 1つ前の要素。またはinitialValue
- currentValue: 現在処理している要素
- index: 配列要素のインデックス
- array: 配列全体
- initialValue: 1回目のループのpreviousValue
簡単にまとめると、配列をループさせて「何らかの処理」を行い、「1つの値」を返す機能。たぶんここまでだとよくわからないと思うので、なぜreduceがあると嬉しいのか実例をあげて解説する。
reduceを使わない場合
reduceを使った場合と比較するために、まずはreduceを使わない場合の例をあげる。
たとえば以下のようなアカウントの配列があったとする。
const accounts = [
{ id: 1, firstName: 'taro', lastName: 'sato', age: 10, sex: 'male' },
{ id: 2, firstName: 'jiro', lastName: 'suzuki', age: 18, sex: 'male' },
{ id: 3, firstName: 'saburo', lastName: 'takahashi', age: 20, sex: 'male' },
{ id: 4, firstName: 'hanako', lastName: 'tanaka', age: 10, sex: 'female' },
{ id: 5, firstName: 'sachiko', lastName: 'kobayashi', age: 20, sex: 'female' }
];
この配列から「男性」「未成年」、「未成年の男性」の「フルネーム」を抽出した場合、以下のようなコードになるだろう。
// not-use-reduce.js
// 男性を抽出するフィルタ
const isMale = account => account.sex === 'male';
// 未成年を抽出するフィルタ
const isUnderage = account => account.age < 20;
// フルネームを生成するフィルタ
const makeFullName = account => account.firstName + ' ' + account.lastName;
// 男性の名前一覧
const males =
accounts
.filter(isMale)
.map(makeFullName);
// 未成年の名前一覧
const underages =
accounts
.filter(isUnderage)
.map(makeFullName);
// 未成年の男性の名前一覧
const unserageMales =
account
.filter(isMale)
.filter(isUnderage)
.map(makeFullName);
条件が増えた分だけ、filterやmap、その他Array系のメソッドをチェーンして実装すればよい。ただし、ここで重要なのはメソッドチェーンした分だけループ回数が増えるということ。
たとえば「未成年の男性のフルネーム一覧」を取得するためには、1回目のfilterで5回、2回目のfilterはすでに男性でフィルタリングされているので3回。フルネームを取得するmapで2回。合計10回ループしている。
今回の要素数は5個なので気にならないが、データが膨大だったり、条件が複雑になると比例してループ回数も増えていく。
ループの回数を減らす方法
そこで考えられるのが、1回のループで「男性」「未成年」「フルネーム」の抽出を1度にやってしまう方法だ。
// forEach.js
const names = [];
accounts.forEach(a => {
if (isMale(a) && isUnderage(a){
names.push(makeFullName(a));
}
});
ループの外に出力用配列を用意しておいて、forEach内でフィルタリングしながらpushしている。これでループ回数を5回に減らすことができた。
もちろんこれで終わり、で十分だ。
以前の私もそうだった。
reduceを使う場合
forEachでも十分なのだが、reduceを使うことでちょっとだけシンプルに実装することができる。
// reduce.js
const names = accounts.reduce((prev, curr, idx, ary) => {
if (isMale(curr) && isUnserage(curr)) {
prev.push(makeFullName(curr);
}
return prev;
}, []);
やっていることはforEachと同じなのだが、initValueに[](空配列)を渡して、reduce内で編集して返している。そのためループの外に出力用の配列を用意する必要がない。
じゃぁ、forEachとreduceの何が違うの?
大前提として、メソッドの意味と使う目的が違う。
forEachは「配列の要素を1つずつ処理する」ことが目的。
reduceは「単一の値を返す」ことが目的。
「未成年の男性のフルネーム一覧を取得する」という例では、forEachよりもreduceの方が「メソッドの目的」にあっている。
で、結局どっちを使えばいいの?
forEach教とreduce教の宗派争いになるので、好みでしかない。
また、データが少ない場合は、filterやmapを使ったほうがコードが読みやすくなる。
実際に書いてみて、読みやすい方を使うのが一番だ。
おまけ: さらに実践的なreduceの使い方
reduceと一緒に語られることが多いのは、Promiseの直列処理だ。
// promise.js
// 指定した時間待機してから関数を実行する
function timer(waitTime, fn) {
return new Promise((resolve, reject) => {
setTimeout(() => {
fn();
resolve();
}, waitTime);
});
}
// reduceを使わない場合
Promise.resolve()
.then(timer(100, () => { console.log('wait 100ms') })
.then(timer(200, () => { console.log('wait 200ms') })
.then(timer(300, () => { console.log('wait 300ms') })
.then(timer(400, () => { console.log('wait 400ms') });
// reduceを使う場合
const timers = [...Array(4)].map((a, i) => {
const waitTime = (i + 1) + 100;
return timer(waitTime, () => { console.log(`wait ${waitTime}ms`) };
});
// timers = [
// timer(100, () => { console.log('wait 100ms') }),
// timer(200, () => { console.log('wait 200ms') }),
// timer(300, () => { console.log('wait 300ms') }),
// timer(400, () => { console.log('wait 400ms') })
// ];
// reduceでPromiseを直列実行する
timers.reduce((prev, curr, i, ary) => {
return prev.then(curr);
}, Promise.resolve());
参考サイト
以上
written by @bc_rikko
0 件のコメント :
コメントを投稿