この記事の概要
コードレビューをしていると、ループ文の使い方、書き方が適切でない事が多いのでよく使われるループ文をまとめました。(何でも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);