15
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

un-T factory! XA Advent Calendar 2020Advent Calendar 2020

Day 7

JavaScriptで2つの配列からそれぞれをキーと値としてオブジェクトを作った話【初心者向け/reduce()入門】

Last updated at Posted at 2020-12-06

#はじめに

JavaScriptでデータを取得して整形することはよくあると思います。

扱いやす形のものばかりなら良いですが、
そうではないのものも多くあるのがこの世の常です。

なので、こちらで扱いやすい形にいじってあげる。
ということも当たり前のように発生します。

あるオブジェクトのキーを取得して配列化したり、
あるオブジェクトの値を取得して配列化したり、
配列から特定の要素に絞り込んで再び配列化したり。。。

そんな中、とあるデータ操作に躓いたので、
解決策を共有します。

当記事を読んでいて配列とかオブジェクトそのものの理解が怪しいよという方は、
拙著の過去記事
https://qiita.com/shigematsu_k/items/19a47ff2a8fa66e23a3a
も読んでみてください。

#本題

ここからはタイトル通りです。
2つの配列があり、
うち1つの配列をキー、
もう1つを値として新たなオブジェクトを作りたい時にハマりました。

下のイメージです。


//オブジェクトのキーにしたい配列
const array1 = ['弾道', 'ミート', 'パワー', '走力', '肩力', '守備力', '捕球'];
//オブジェクトの値にしたい配列
const array2 = [4, 'C', 'A', 'B', 'A', 'D', 'E'];

//2つの配列を使ってこんなオブジェクトを作りたい。
const giita = {
  弾道: 4,
  ミート: C,
  パワー: A,
  走力: B,
  肩力: A,
  守備力: D,
  捕球: E
}

ゴールはQiitaではなくギータです。
(ホークス優勝おめでとう。)

#解決策

いきなり結論です。
今回はreduce()メソッドを使って解決しました。

以下が解決結果となります。

const array1 = ['弾道', 'ミート', 'パワー', '走力', '肩力', '守備力', '捕球'];
const array2 = [4, 'C', 'A', 'B', 'A', 'D', 'E'];

const giita = array2.reduce((accumulator, currentValue, index) => {
  accumulator[array1[index]] = currentValue;
  return accumulator;
}, {});

console.log(giita);

console.log(giita);の出力結果

Object {
  パワー: "A",
  ミート: "C",
  守備力: "D",
  弾道: 4,
  捕球: "E",
  肩力: "A",
  走力: "B"
}

はい。
これでオブジェクトにできました。(たった4行!)
オブジェクトなので、並び順は良いとして
ただ、どういう理屈でこうなったのか。
全くわかってません。

全くわかってないまま実装するのはヒエーッですよね。(ヒエーッ)
何がどうなってこのような結果が得られているのかは理解したいです。
なので少し調べます。

#正体不明のreduce()メソッド

reduce() メソッドは、配列の各要素に対して (引数で与えられた) reducer 関数を実行して、単一の出力値を生成します。

reducer 関数は 4 つの引数を取ります。
アキュムレーター (acc)
現在値 (cur)
現在の添字 (idx)
元の配列 (src)
reducer 関数の返値はアキュムレーターに代入され、配列内の各反復に対してこの値を記憶します。最終的に単一の結果値になります。

引数
callback
配列のすべての要素 (initialValue が提供されなかった場合は、最初を除く) に対して実行される関数です。
これは 4 つの引数を取ります。

 accumulator
callback の返値を蓄積するアキュームレーターです。これは、コールバックの前回の呼び出しで返された値、あるいは initialValue が指定されている場合はその値となります。
 currentValue
現在処理されている配列の要素です。
 index (Optional)
現在処理されている配列要素のインデックスです。initialValue が指定された場合はインデックス 0 から、そうでない場合はインデックス 1 から開始します。
 array (Optional)
reduce() が呼び出された配列です。

initialValue (Optional)
callback の最初の呼び出しの最初の引数として使用する値。initialValue が与えられなかった場合、配列の最初の要素がアキュムレーターの初期値として使用され、currentValue としてスキップされます。空の配列に対して reduce() を呼び出した際、initialValue が指定されていないと TypeError が発生します。

引用元:MDN web docs Array.prototype.reduce()1

:thinking::thinking::thinking::thinking::thinking:

ナンジャコレ状態です。はい。

ある程度読み進めると、reduce()は本当になんでもできそうなヤバいやつということがわかります。
それと同時に、凡人には一度に全てを理解するのは不可能ということもわかります。

なので、とりあえず今回の処理だけ追っかけましょう。

#今回のケースのreduce()メソッド

上記の引用文を分解して追っかけることにします。
ポイントは2つです。

##ポイントその1 第一引数accumulator

ポイントその1です、
引用文を読んでみると

配列内の各反復に対してこの値を記憶します。

とあります。

また、reduce()メソッドの第1引数であるaccumulator
についての記述箇所を読むと、

accumulator
callback の返値を蓄積するアキュームレーターです。これは、コールバックの前回の呼び出しで返された値、あるいは initialValue が指定されている場合はその値となります

ということなので、
どうやらコールバックの第1引数であるアキュームレーターに返値が蓄積(記憶)され、

最終的に単一の結果値になります

最終的に1つの値となっているようだとわかります。

実際どう蓄積(記憶)しているのか
reduce()に渡した各引数の状態をconsole.logで覗きます。
(今回の場合は第4引数であるarrayは渡しておりません。)

const array1 = ['弾道', 'ミート', 'パワー', '走力', '肩力', '守備力', '捕球'];
const array2 = [4, 'C', 'A', 'B', 'A', 'D', 'E'];

const giita = array2.reduce((accumulator, currentValue, index) => {
  accumulator[array1[index]] = currentValue;

  //覗いてみよう!
  console.log(accumulator,currentValue,index);
  
  return accumulator;
}, {});

console.log(accumulator,currentValue,index);の出力結果

Object {
  弾道: 4
} 4 0

Object {
  ミート: "C",
  弾道: 4
} "C" 1

Object {
  パワー: "A",
  ミート: "C",
  弾道: 4
} "A" 2

Object {
  パワー: "A",
  ミート: "C",
  弾道: 4,
  走力: "B"
} "B" 3

Object {
  パワー: "A",
  ミート: "C",
  弾道: 4,
  肩力: "A",
  走力: "B"
} "A" 4

Object {
  パワー: "A",
  ミート: "C",
  守備力: "D",
  弾道: 4,
  肩力: "A",
  走力: "B"
} "D" 5

Object {
  パワー: "A",
  ミート: "C",
  守備力: "D",
  弾道: 4,
  捕球: "E",
  肩力: "A",
  走力: "B"
} "E" 6

はい、見えてきました。

array2をindex順に回しています。
そしてaccumulatorのキーとして、array1[index]を設定し、そこに現在値を値として格納しているわけです。

もっと具体的にいうと、
初回処理の引数indexは0で、現在値(currentValue)は4になっています。
そして
accumulator[array1[index]] = currentValue;
accumulatorのキーとして、array1[0]つまり'弾道'に現在値である4を格納して初回の処理を終えます。

このように処理をarray2.lengthの数だけ記憶しながら続け、最終的な結果を返す。ということですね。

##ポイントその2 initialValue

ポイントその1でどういう処理が行われているかはつかめました。

しかしながら、
そもそもなぜオブジェクト型で蓄積され、
最終的に欲しい形を得られているのでしょうか。

そこで、
もう1つ重要なポイントが**initialValue**です。
引用文該当箇所は下記の通り。

initialValue (Optional)
callback の最初の呼び出しの最初の引数として使用する値。initialValue が与えられなかった場合、配列の最初の要素がアキュムレーターの初期値として使用され、currentValue としてスキップされます。空の配列に対して reduce() を呼び出した際、initialValue が指定されていないと TypeError が発生します。

この場合のcallback の最初の呼び出しの最初の引数として使用する値として設定されているものは、
一体なんでしょうか。

const giita = array2.reduce((accumulator, currentValue, index) => {
  accumulator[array1[index]] = currentValue;
  return accumulator;
//↓↓initialValueに注目↓↓
}, {});

そうです。空のオブジェクトです。
初回処理の引数として空のオブジェクトを渡すことで、
その中で、キーを設定・値を格納を蓄積し1つの値として返す、
ということになり求めているオブジェクトが得られているわけですね。

実際のコードは割愛しますが、initialValueを設定していない場合の
giitaはただ単に配列の最初の値(array2[0])を返すだけとなり出力結果は4となります。

ここまで確認できたら今回の場合は大丈夫。
なんとか理解が実装に耐えられそうです。

##おまけ

もし、array1とarray2でlengthが異なる場合はどうでしょうか。
一応確認しましょう。

###array2に値が1つ多い場合

const array1 = ['弾道', 'ミート', 'パワー', '走力', '肩力', '守備力', '捕球'];
//array2に1つ追加
const array2 = [4, 'C', 'A', 'B', 'A', 'D', 'E','F'];

const giita = array2.reduce((accumulator, currentValue, index) => {
  accumulator[array1[index]] = currentValue;
  
  return accumulator;
}, {});

  console.log(giita);

console.log(giita);の出力結果

Object {
  undefined: "F",
  パワー: "A",
  ミート: "C",
  守備力: "D",
  弾道: 4,
  捕球: "E",
  肩力: "A",
  走力: "B"
}

この場合array2に対してreduce()処理を加えているので、
当然1つ追加されたもの(この場合'F')も値としてキーに格納しようとしますが、
キーが定義されていない状態なのでこのような出力結果になります。

###array1に値が1つ多い場合
逆の場合はこのようになります。

//array1に1つ追加
const array1 = ['弾道', 'ミート', 'パワー', '走力', '肩力', '守備力', '捕球','ファンサービス'];
const array2 = [4, 'C', 'A', 'B', 'A', 'D', 'E'];

const giita = array2.reduce((accumulator, currentValue, index) => {
  accumulator[array1[index]] = currentValue;
  
  return accumulator;
}, {});

  console.log(giita);

console.log(giita);の出力結果

Object {
  パワー: "A",
  ミート: "C",
  守備力: "D",
  弾道: 4,
  捕球: "E",
  肩力: "A",
  走力: "B"
}

はい。
array2.length分しか処理は走らないのでキーとして使いたい配列array1の最後の値(この場合'ファンサービス')は当然無視されます。

#最後に

reduce()メソッドは調べていると、本当になんでもできそうです。
配列オブジェクトの持つメソッドの中でも最強と言えるかもしれません。。

配列を扱うとき、他のメソッドに頼らずreduce()で全てに対応できれば・・
なんて夢想しつつ今後も勉強していきたいです。

最初はなんだか良くわからなかったアキュームレーターについて調べると、
どうやら蓄圧器のことのようです。
もっとよくわからないですね!(ヒエーッ)
蓄積して圧縮する。くらいのノリでとらえておきます。
(MDNの記事もアキュムレーターとアキュームレーターで表記揺れしてるし..ブツブツ...)
参考リンクに貼っておきます。

##参考リンク

https://kde.hateblo.jp/entry/2018/10/13/065738#Arrayprototypereduce-%E3%81%AE%E6%A6%82%E8%A6%81
https://jsprimer.net/basic/loop/#const-iteration
https://ja.wikipedia.org/wiki/アキュムレータ_(機械)

  1. https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce

15
4
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?