search
LoginSignup
11
Help us understand the problem. What are the problem?

posted at

updated at

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

この記事の概要

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

演習問題

動作確認用:
https://stackblitz.com/edit/angular-1wswec?embed=1&file=src/app/app.component.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の要素自体は変更しないこと

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をグルーピングして合計する

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

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の方が適切

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

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の存在を知らなかった時にやってたダサいやり方

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

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

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の存在を知らなかった時にやってたダサいやり方

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

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の存在を知らなかった時にやってたダサいやり方

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

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の存在を知らなかった時にやってたダサいやり方

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

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

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の存在は知ってた時にやってたダサいやり方

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

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)
}

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

// 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;
});

ログ結果&解説

===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のレコードは抽出される

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

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

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);
}

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

// 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になる。

ログ結果&解説

===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

こんな書き方も可能。

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

ログ結果

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

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

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

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

詳しく

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);

ログ結果

===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

// 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 | アロー関数の省略形

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
  }
}

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

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

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

関連記事

for (let i = 0; i < 10; i++){} 文を辞める

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
What you can do with signing up
11
Help us understand the problem. What are the problem?