現場でよく使うJavascriptループ文まとめ

## この記事の概要
コードレビューをしていると、ループ文の使い方、書き方が適切でない事が多いのでよく使われるループ文をまとめました。（何でもforEachで済ませちゃうとか）

## 演習問題

動作確認用：
https://stackblitz.com/edit/angular-1wswec?embed=1&file=src/app/app.component.ts

```ts
interface SampleData {
  id: number;
  category: string;
  tag: string;
  lot: number
}

const list1: SampleData[] = [
  {id: 1, category: 'A', tag: 'AA', lot: 10},
  {id: 2, category: 'A', tag: 'AA', lot: 20},
  {id: 3, category: 'B', tag: 'AA', lot: 30},
  {id: 4, category: 'B', tag: 'AA', lot: 40},
  {id: 5, category: 'C', tag: 'AA', lot: 50}
];
```

#### Q1.list1の各要素のlotを2倍にする

#### Q2.list1を元に下記の新しいリストを作成する。 ※list1の要素自体は変更しないこと

```ts
const list2 = [
  {id: 1, place: 'A', lot: 30},
  {id: 2, place: 'A', lot: 60},
  {id: 3, place: 'B', lot: 90},
  {id: 4, place: 'B', lot: 120},
  {id: 5, place: 'C', lot: 150}
];
```

#### Q3.list1内の全てのレコードのtagがAAであるか判定する

#### Q4.list1からcategoryがBのレコードのみ抽出したリストを作成する

#### Q5.list1からlotが20未満のレコードが存在するか確認する

#### Q6.list1からidが2のレコードを抽出する

#### Q7.list1からcategoryの重複を除去する

#### Q8.list1のlotを全合計した値を取得する

#### Q9.list1から下記の結果になるようにcategoryごとにlotをグルーピングして合計する

```ts
const list2 = [
  {category: 'A', totallot: 30},
  {category: 'B', totallot: 70},
  {category: 'C', totallot: 50},
];
```


## 回答例

#### A1.list1の各要素のlotを2倍にする

forEachを使う。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

```ts
this.list1.forEach(val => {
  val.lot = val.lot * 2;
});
console.log('list1: ', this.list1);

// 省略形
this.list1.forEach(val => val.lot = val.lot * 2);
```

※forEachではなくmapでも一応可。でも返す値は不要なのでforEachの方が適切

```ts
this.list1.map(val => val.lot = val.lot * 2);
console.log('list1(ダサ): ', this.list1);
```

#### A2.list1を元に新しいリストを作成する

mapを使う。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/map

```ts
const list2 = this.list1.map(val => {
  return {
    id: val.id,
    place: val.category,
    lot: val.lot * 2    
  };
);
console.log('list1: ', this.list1);
console.log('list2: ', list2);

// 省略形　※{}をreturnする時は()が必要
const list2 = this.list1.map(val => ({id: val.id, place: val.category, lot: val.lot * 2}));

// さらにリファクタリング
private hoge(): void {
  const list2 = this.list1.map(this.mapper);
}

private mapper(val: SampleData): {id: number, place: string, lot: number} {
  return {
    id: val.id,
    place: val.category,
    lot: val.lot * 2    
  };
}
```

mapの存在を知らなかった時にやってたダサいやり方

```ts
const list2 = [];
this.list1.forEach(val => {
  const newVal = this.mapper(val);
  list2.push(newVal);
});
console.log('list1(ダサ): ', this.list1);
console.log('list2(ダサ): ', list2);
```

#### A3.list1内の全てのレコードのtagがAAであるか判定する

everyを使う。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/every

```ts
const isAllAA = this.list1.every(val => {
  return val.tag === 'AA';
});
console.log('isAllAA?: ', isAllAA);

// 省略形
const isAllAA = this.list1.every(val => val.tag === 'AA');

// さらにリファクタリング
private hoge() { 
  const isAllAA = this.list1.every(this.isTagAA);
}

private isTagAA(val: SampleData): boolean {
 return val.tag === 'AA';
}
```

#### A4.list1からcategoryがBのレコードのみ抽出したリストを作成する

filterを使う。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

```ts
const list2 = this.list1.filter(val => {
  return val.category === 'B';
});
console.log('list2: ', list2);

// 省略形
const list2 = this.list1.filter(val => val.category === 'B');

// さらにリファクタリング
private hoge() {
  const list2 = this.list1.filter(this.isCategoryB);
}

private isCategoryB(val: SampleData): boolean {
  return val.category === 'B';
}
```

filterの存在を知らなかった時にやってたダサいやり方

```ts
const list2 = [];
this.list1.forEach(val => {
  if (val.category === 'B') {
    list2.push(val);
  }
});
console.log('list2(ダサ): ', list2);
```

#### A5.list1からlotが20未満のレコードが存在するか確認する

someを使う。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/some

```ts
const existsUnder20 = this.list1.some(val => {
  return val.lot < 20;
});
console.log('existsUnder20?: ', existsUnder20)

// 省略形
const existsUnder20 = this.list1.some(val => val.lot < 20);

// さらにリファクタリング
private hoge() {
  const existsUnder20 = this.list1.some(this.isUnder20);
}

private isUnder20(val: SampleData): boolean {
  return val.lot < 20;
}
```

someの存在を知らなかった時にやってたダサいやり方

```ts
let existsUnder20 = false;
this.list1.forEach(val => {
  if (20 > val.lot) {
    existsUnder20 = true; // 合致してもループは止まらない
  }
});
console.log('existsUnder20(ダサ)?: ', existsUnder20)
```

#### A6.list1からidが2のレコードを抽出する

findを使う。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/find

```ts
const target = this.list1.find(val => {
  return val.id === 2;
});
console.log('target: ', target);

// 省略形
const target = this.list1.find(val => val.id === 2);

// さらにリファクタリング
private hoge() {
  const target = this.list1.find(this.isId2);
}
private isId2(val: SampleData): boolean {
  return val.id === 2;
}
```

findの存在を知らなかった時にやってたダサいやり方

```ts
const target = this.list1.filter(val => val.id === 2)[0];
console.log('target(ダサ): ', target);
```

これはJavaのfilter(・・・).findFirst()と違って2に合致したレコードを見つけてもループは止まらない

```ts
const target = this.list1.find((val, idx) => {
  console.log('idx: ', idx);
  return val.id === 2;
});
/*
 ログ：
 idx:  0
 idx:  1
*/

const target = list1.filter((val, idx) => {
  console.log('idx(ダサ): ', idx);
  return val.id === 2;
})[0];
/*
 ログ：
 idx(ダサ):  0
 idx(ダサ):  1
 idx(ダサ):  2
 idx(ダサ):  3
 idx(ダサ):  4
*/
```

findの存在を知らなかったけどsomeの存在は知ってた時にやってたダサいやり方

```ts
let target;
this.list1.some(val => {
  if(val.id === 2){
    target= val;
    return true; // これでループは止まる
  }
});
console.log('target(ダサ2): ', target);
```

#### A7.list1からcategoryの重複を除去する

filterとfindIndexを使う。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex

```ts
const list2 = list1.filter((val1, idx, array) => {
  const matchIdx = array.findIndex(val2 => {
    return val2.category === val1.category;
  });
  return idx === matchIdx;
});
console.log('list2: ', list2)

// 省略形
const list2 = this.list1.filter((val1, idx, array) =>
  idx === array.findIndex(val2 => val2.category === val1.category)
);

// さらにリファクタリング
private hoge() {
  const list2 = this.list1.filter(this.nonDuplicateCategory);
}

private nonDuplicateCategory(val1: SampleData, idx: number, array: SampleData[]): boolean {
  return idx === array.findIndex(val2 => val2.category === val1.category)
}
```

ちょっと難しいのでログ出力。

```ts
// arrayはlist1自身
const list2 = list1.filter((val1, idx, array) => {

  // findIndexは合致する要素があればそのindex、合致しなければ-1を返す。
  const matchIdx = array.findIndex(val2 => {
    return val2.category === val1.category;
  });

  console.log('===idx: ', idx, '===');
  console.log('val1: ', val1);
  console.log('matchIdx: ', matchIdx);

  return matchIdx === idx;
});
```

ログ結果&解説

```txt
===idx: 0===
val1: {id:1, category: "A", ・・・}
matchIdx: 0 // 0番目の要素と合致した。現在のidxとも合致するのでこのid1のレコードは抽出される
===idx: 1===
val1: {id:2, category: "A", ・・・}
matchIdx: 0 // 0番目の要素と合致したが、現在のidxと合致しないのでこのid2のレコードは抽出されない
===idx: 2===
val1: {id:3, category: "B", ・・・}
matchIdx: 2 // 2番目の要素と合致した。現在のidxとも合致するのでこのid3のレコードは抽出される
===idx: 3===
val1: {id:4, category: "B", ・・・}
matchIdx: 2 // 2番目の要素と合致したが、現在のidxと合致しないのでこのid4のレコードは抽出されない
===idx: 4===
val1: {id:5, category: "C", ・・・}
matchIdx: 4 // 4番目の要素と合致した。現在のidxとも合致するのでこのid5のレコードは抽出される
```

この方法知らなかった時のダサいやり方

```ts
const list2: {id: number, category: string, tag: string, lot: number}[] = [];
this.list1.forEach(val1 => {
  if (!list2.some(val2 => val2.category === val1.category)) {
    list2.push(val1);
  }
});
console.log('list2(ダサ): ', list2);
```

#### A8.list1のlotを全合計した値を取得する

reduceを使う。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce

```ts
const result = this.list1.reduce((acc, cur) => {
  return (acc += cur.lot);
}, 0);
console.log('result: ', result);

// 省略形
const result = this.list1.reduce((acc, cur) => acc += cur.lot, 0);

// さらにリファクタリング
private hoge() {
  const result = this.list1.reduce(this.sumLot, 0);
}

private sumLot(acc: number, cur: SampleData) {
  return (acc += cur.lot);
}
```

ログ出力で挙動を詳しく見てみる。

```ts
// accはaccumulatorの略。ループ中の処理の結果を保持する。
// curはcurrentValueの略。現在のループ中の値になる。
const result = this.list1.reduce((acc, cur, idx) => {
  console.log('===idx: ', idx, '===');
  console.log('acc: ', acc);
  console.log('cur.lot: ', cur.lot);
  const total = acc + cur.lot;
  console.log('total: ', total);
  return total;
}, 0); // この0はaccの初期値。これでaccの型がnumberになる。
```

ログ結果&解説

```txt
===idx: 0===
acc: 0 // まだ加算処理が行われてないので初期値0
cur.lot: 10 // 現在処理中のlotは10
total: 10 // 0 + 10
===idx: 1===
acc: 10 // 前回のループの計算結果が保持されている
cur.lot: 20
total: 30
===idx: 2===
acc: 30 // 前回までのループの計算結果が保持されている
cur.lot: 30
total: 60
===idx: 3===
acc: 60
cur.lot: 40
total: 100
===idx: 4===
acc: 100
cur.lot: 50
total: 150
```

こんな書き方も可能。

```ts
// currentValueのところを{}で括って必要なプロパティを書くと、直接要素を取得できる。
const result2 = this.list1.reduce((acc, { id, lot }, idx) => {
  console.log('===id: ', id, '===');
  console.log('lot: ', lot);
  return acc += lot;
}, 0);
```

ログ結果

```txt
===id: 1 ===
lot: 10
===id: 2 ===
lot: 20
===id: 3 ===
lot: 30
===id: 4 ===
lot: 40
===id: 5 ===
lot: 50
```

reduceの存在を知らなかった時にやってたダサいやり方

```ts
let result = 0;
this.list1.forEach(val => result += val.lot);
console.log('result(ダサ): ', result);
```

#### A9.list1からcategoryごとにlotをグルーピングして合計する

今のところググるとreduceを使った書き方が一番良い方法として出てくる。
https://stackoverflow.com/questions/14446511/most-efficient-method-to-groupby-on-an-array-of-objects

```ts
const categoryMap = this.list1.reduce((acc, cur) => {
  const accTotal = acc.get(cur.category);
  const total = cur.lot + (accTotal || 0);
  return acc.set(cur.category, total);
}, new Map<string, number>());

const list2 = Array.from(categoryMap, x => {
  return {
    category: x[0],
    totalLot: x[1]
  };
});

console.log('list2: ', list2);

// さらにリファクタリング
private hoge(): void {
  const map = this.list1.reduce(this.mapper, new Map<string, number>());
  const list2 = Array.from(map, ([category, totalLot]) => ({category, totalLot}));
}

private mapper(acc, cur): Map<string, number> {
  const accTotal = acc.get(cur.category);
  const total = cur.lot + (accTotal || 0);
  return acc.set(cur.category, total);
}
```

※Map
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map

詳しく

```ts
const map = this.list1.reduce((acc, cur, idx) => {
  console.log('===idx: ', idx, '===');
  console.log('acc: ', acc);
  console.log('category: ', cur.category);
  console.log('lot: ', cur.lot);

  const accTotal = acc.get(cur.category);
  console.log('accTotal: ', accTotal);

  const total = cur.lot + (accTotal || 0);

  // Map型にset：キー(category)が重複すればそのキー値のtotalが上書きされるだけ
  // キーが重複しなければ追加される
  return acc.set(category, total);

}, new Map<string, number>()); // accの初期値はnew Map()

console.log('=========');
console.log('map: ', map);
```

ログ結果

```txt
===idx: 0 ===
acc: Map(0) {}
category: A
lot: 10
accTotal: undefined // accにcategoryAは入っていない
===idx: 1 ===
acc: Map(1) {"A" => 10}
category: A
lot: 20
accTotal: 10
===idx: 2 ===
acc: Map(1) {"A" => 30} // Aを上書きしただけなのでまだ1ペアしかない
category: B
lot: 30
accTotal: undefined // accにcategoryBは入っていない
===idx: 3 ===
acc: Map(2) {"A" => 30, "B" => 30}
category: B
lot: 40
accTotal: 30
===idx: 4 ===
acc: Map(2) {"A" => 30, "B" => 70}
category: C
lot: 50
accTotal: undefined
=========
map: Map(3) {"A" => 30, "B" => 70, "C" => 50}
```

#### Array.from

配列的に値を持っているくせにArray型ではないものをArray型に変換する。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/from

```ts
// Map型から配列に変換
// Map<string, number>の1レコードは[string, number]
const list2 = Array.from(map, (x: [string, number]) => {
  return {
    category: x[0],
    totalLot: x[1]
  }
}); 

// こんな書き方もできる
const list2 = Array.from(map, ([category, totalLot]) => {
  return {
    category: category,
    totalLot: totalLot
  };
}); 

// JSONはキーと変数名が同じなら「:値」省略可能
const list2 = Array.from(map, ([category, totalLot]) => ({category, totalLot}));
```

lodashっていうライブラリ使うとgroupByがあるらしい。
標準にもgroupByください。
https://qiita.com/takutakuma/items/04f71881a59089404348

## Tips | アロー関数の省略形

```ts
hoge(): void {
  const list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  // フル
  const newList = list.map((num: number) => {
    return this.mapper(num);
  });

  return newList;
}

mapper(num: number): {id: number, data:number} {
  return {
     id: num,
     data: num * 10
  }
}
```
↓

```ts
// 引数の型定義は任意。引数が1つであれば()省略
list.map(num => {
  return this.mapper(num);
});  
```

↓

```ts
// 関数のbodyが1文であれば {}、return、; 省略
list.map(num => this.mapper(num));
```

↓

```ts
// 呼ぶ関数に渡す引数が同じであれば関数を定義するだけ
list.map(this.mapper);
```

## 関連記事

[for (let i = 0; i < 10; i++){} 文を辞める](https://qiita.com/pg_yamaton/items/ac60688f00e3ab2398ff)