2017/06/20

もう怖くない!Array.prototype.reduceを理解して実践的に使う

Array.prototype.reduceを説明する際、必ずといってよいほど以下のような数字を順に足していくだけのサンプルコードが添えられている。
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 件のコメント :

コメントを投稿