KDE BLOG

Webデザインやコーディングについて書いています

【JavaScript基礎】Array.prototype.reduce() をしっかり理解する&サンプル集

配列のメソッドの中でも個人的にとっつきにくくて苦手意識のあった Array.prototype.reduce()
しかしAPIから取得したデータを扱いやすいように整形できたりと、使いこなせればとても強力なツールになることは間違いなく、この苦手意識を克服するために理解を深めつつ、いくつかのサンプルを作ってみたいと思います。

Array.prototype.reduce() の概要

ざっくりいうと、配列からひとつの値を求めるときに使われます。

reduce メソッドは2つずつ要素を取り出し(左から右へ)、その値を コールバック関数 に適用し、 次の値として1つの値を返します。 最終的な reduce メソッドの返り値は、コールバック関数が最後に return した値となります。
(ループと反復処理 · JavaScriptの入門書 #jsprimer より)

なおEcmaScript 5のメソッドのためIE9から使えます。
http://kangax.github.io/compat-table/es5/#test-Array.prototype.reduce

構文

const result = array.reduce((前回の値, 現在の値, 現在currentとして処理されている要素のインデックス, reduceによって操作されている配列自身) => {
    return 次の値;
}, 初期値);

言葉だけだと分かりにくいですね。
よくあるサンプルは下記になります。
「初期値あり」と「初期値なし」で挙動が変わるので比較してみてみます。

const nums = [1, 2, 3, 4, 5];

// 初期値あり
const result1 = nums.reduce((prev, current, index, array) => {
  console.log(prev, current, index, array);
  return prev + current;
}, 0);
console.log(result1); // 15

// 初期値なし
const result2 = nums.reduce((prev, current, index, array) => {
  console.log(prev, current, index, array);
  return prev + current;
});
console.log(result2); // 15 

ループ中で行っているconsole.log(prev, current, index, array); の結果は下記のようになります。

▼初期値あり

ループ週目 prev current index array
1 0 1 0 [1, 2, 3, 4, 5]
2 1 2 1 [1, 2, 3, 4, 5]
3 3 3 2 [1, 2, 3, 4, 5]
4 6 4 3 [1, 2, 3, 4, 5]
5 10 5 4 [1, 2, 3, 4, 5]

▼初期値なし

ループ週目 prev current index array
1 1 2 1 [1, 2, 3, 4, 5]
2 3 3 2 [1, 2, 3, 4, 5]
3 6 4 3 [1, 2, 3, 4, 5]
4 10 5 4 [1, 2, 3, 4, 5]

この結果から次のことが分かります。

初期値あり 初期値なし
prev ・初回ループの時は 初期値 が入る
・2週目以降は処理結果(returnされた値)が入る
・初回ループの時に 配列の最初の要素 が入る
・2週目以降は同左
current 配列の最初の要素から順に入る 配列の2番目の要素から順に入る
index current で処理されている要素のインデックス 同左
array reduce() が実行されている配列 同左

初期値prevcurrent がどうなるかがネックになります。
この流れを図にしてみました。

f:id:jinseirestart:20181010040348p:plain

原則、初期値はつけた方が良い

なお、reduce() のコールバック関数の第三引数以降(indexarray)と、reduce()の第二引数である 初期値 はオプションです。

ただ 初期値 に関しては、空の配列を 初期値なし で実行するとエラーになったり、予期せぬ実行結果になったりする可能性があるため、基本的には 初期値 は設定した方が良い ようです。
Array.prototype.reduce() - JavaScript | MDN

サンプル集

配列をオブジェクトにする

const data = [
  { name: 'Taro', age: 20 },
  { name: 'Hanako', age: 25 },
  { name: 'Tom', age: 30 }
];

/* こういう形にしたい
{
  Taro: 20,
  Hanako: 25,
  Tom: 30
}
*/

const result = data.reduce((prev, current) => {
  prev[current.name] = current.age;
  return prev;
}, {});

console.log(result); // OK {Taro: 20, Hanako: 25, Tom: 30}

配列をインデックス付きのオブジェクトにする

const data = [
  { name: 'Taro', age: 20 },
  { name: 'Hanako', age: 25 },
  { name: 'Tom', age: 30 }
];

/* こういう形にしたい
{
  1: { name: 'Taro', age: 20 },
  2: { name: 'Hanako', age: 25 },
  3: { name: 'Tom', age: 30 }
}
*/
const result = data.reduce((prev, current, index) => {
  prev[index + 1] = current;
  return prev;
}, {});

console.log(result); 
/* 実行結果
 { 
  1: {name: "Taro", age: 20},
  2: {name: "Hanako", age: 25},
  3: {name: "Tom", age: 30}
}
*/

配列内の要素から不要な値を削除する

const data = [
  { id: 1, name: 'Taro', age: 20, country: 'Japan'},
  { id: 2, name: 'Yan', age: 30, country: 'China'},
  { id: 3, name: 'Bob', age: 40, country: 'America'},
];

// data 内にある各要素のオブジェクトのキーを、name と country だけにしたい
const result = data.reduce((prev, current) => {
  const { name, country } = current;
  prev.push({ name, country });
  return prev;
}, []);

console.log(result);
/* 処理結果
[
  {name: "Taro", country: "Japan"}, 
  {name: "Yan", country: "China"}, 
  {name: "Bob", country: "America"}
]
*/

ここのreduce内の処理では分割代入を用いることで簡潔に書くことが可能です。
分割代入に関しては以前にまとめた記事があります。
【ES2015】分割代入の基本と便利な使い方 - KDE BLOG

二次元配列を一次元配列にする

const data = [
  [1, 2, 3], [4, 5, 6], [7, 8, 9]
];

const result = data.reduce((prev, current) => {
  return [...prev, ...current];
}, []);
console.log(result); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

多次元配列を一次元配列にする(再帰

const data = [
  [1, 2, 3, [4, 5], [6, 7, [8, 9]]], 
];

/**
 * 二次元配列を1次元配列にする(シャローコピー)
 */
const mergeAry = (ary) => {
  const result = ary.reduce((prev, current) => {
    return prev.concat(current);
  }, []);
  return result;
};

/**
 * 配列要素の中に配列があるか
 */
const hasArray = (ary) => ary.some(item => Array.isArray(item));

/**
 * 配列がある限り再帰で配列を結合する
 */
const mergeAll = (ary) => {
  if (!hasArray(ary)) {
    return ary; 
  }
  const result = mergeAry(ary);
  return mergeAll(result);
};

console.log(mergeAll(data)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

// こんなのでも大丈夫
const data2 = [[1], [], [2, [3, [4]]], [[5, 6, [7], [8, 9]]]];
console.log(mergeAll(data2)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

再帰関数と reduce() を組み合わせるとかなり複雑な変形が可能です。
私はまだあまり複雑なことができませんが、他にできるようになったら追記していきたいと思います。

条件にマッチする要素から値を出す

const accounts = [
  { id: 1, firstName: 'taro',    lastName: 'sato',      age: 10, sex: 'male' },
  { id: 2, firstName: 'jiro',    lastName: 'suzuki',    age: 28, sex: 'male' },
  { id: 3, firstName: 'saburo',  lastName: 'takahashi', age: 19, sex: 'male' },
  { id: 4, firstName: 'hanako',  lastName: 'tanaka',    age: 10, sex: 'female' },
  { id: 5, firstName: 'sachiko', lastName: 'kobayashi', age: 20, sex: 'female' }
];

// accounts をもとにして、未成年男性のフルネームを配列で取得したい

// 男性か判定
const isMale = account => account.sex === 'male';
// 未成年か判定
const isUnderage = account => account.age < 20;
// フルネーム取得
const getFullName = account => `${account.firstName} ${account.lastName}`;

const result= accounts.reduce((prev, current, i) => {
  if (isMale(current) && isUnderage(current)) {
    prev.push(getFullName(current));
  }
  return prev;
}, []);

console.log(result); // ["taro sato", "saburo takahashi"]

配列内の重複している値を除外する

const ary = [1, 2, 2, 3, 1, 'a', 'b', 'b'];

// 配列内にすでにその値があるか
const hasValue = (value, ary) => ary.some(item => item === value);

const result = ary.reduce((prev, current) => {
  if (!hasValue(current, prev)) {
    prev.push(current);
  }
  return prev;
}, []);

console.log(result); // [1, 2, 3, "a", "b"]

filter() + map() の代わりに使う

React.jsにおける JSX ではJSX.Elementの作成にmap() をよく使いますが、map() は入力&出力は要素数は変わりません。
そのため配列の一部要素は除外したいとなると、filter() を適用させる必要があります。
それでも基本的には問題ありませんが、filter()map() でそれぞれループしてしまいます。 そのようなことが気になるとき、reduce() を使えば解決できます。

const data = [
  { id: 1, name: 'taro'},
  { id: 2, name: 'hanako'},
  { id: 3, name: 'tom'},
];

// 除外判定(idが2の場合は除外)
const isExclude = item => item.id === 2;

// テンプレート
const template = item => `<li>id: ${item.id} / name: ${item.name}</li>`;

// パターン1 (filter + map)
const result1 = data.filter(item => !isExclude(item)).map(template);

// パターン2 (reduce)
const result2 = data.reduce((prev, current) => {
  if (!isExclude(current)) {
    prev.push(template(current));
  } 
  return prev;
}, [])

console.log(result1); // ["<li>id: 1 / name: taro</li>", "<li>id: 3 / name: tom</li>"]
console.log(result2); // ["<li>id: 1 / name: taro</li>", "<li>id: 3 / name: tom</li>"]

上記の場合は、パターン1 (filter + map) の方がシンプルにまとまっていますが、場合によっては reduce の方がシンプルにまとまることがあると思います。

テンプレートリテラルのタグ関数で文字列の組み立てに使う

const name = 'tom';
const country = 'japan';
const food = 'sushi';

/* 変数の先頭一文字を大文字にして展開したい
upperVariables `My name is ${name}. I live in ${country}. I like ${food}.`
出力結果 -> My name is Tom. I live in Japan. I like Sushi.
*/

/**
 * 先頭1文字を大文字にする
 */
function upperCharAtFirst (str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

/**
 * タグ関数:変数内の文字列の先頭1文字を大文字にする
 */
function upperVariablesCharAtFirst (strings, ...values) {
  // 先頭1文字が大文字になった文字列の配列
  const textsUppered = values.reduce((prev, current) => {
    prev.push(upperCharAtFirst(current));
    return prev;
  }, []);

  // strings (中身:["My name is ", ". I live in ", ". I like ", "."] ) と
  // textsUppered (中身:["Tom", "Japan", "Sushi"] ) を組み合わせて返す
  return strings.reduce((prev, current, index) => {
    return prev + current + (textsUppered[index] ? textsUppered[index] : '');
  }, '');
}

console.log(upperVariables `My name is ${name}. I live in ${country}. I like ${food}.`);
// My name is Tom. I live in Japan. I like Sushi.

テンプレートリテラルタグ機能についてはこちらの記事にまとめてあります。
【ES2015】テンプレートリテラルのタグについて - KDE BLOG

まとめ

今回しっかり Array.prototype.reduce() と向き合ってみて感じたのは、そこまで難しくなかったということと、とても実用的だということです。
むしろ慣れてくるとかなり多用しそうなくらい便利だと感じています。

サンプルで紹介したものは、別に reduce() を使わずとも forEach()for 文 で実現できます。
しかし、forEach()for 文は「配列の要素を1つずつ処理する」というのが本来の目的です。
reduce() は「単一の値を返す」のが目的のため、 今回のものは reduce() を使う方が適しているといえます。
関数型プログラミングを行う上では大事な考え方になるようです。

また、 reduce() はループ処理のためによくある一時的な変数を用意する必要もない上、元の配列に変更を加えないメソッドなので使い勝手が良く、今ではこのメソッドが好きになれました。

参考