どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。
前回のおさらい
前回は、「列挙の実行中にオブジェクトを変更しない」をお届けしました。
今日は3回目で、 「複数のループよりも反復メソッドが好ましい」 をお届けします。
テキスト第5章「配列とディクショナリ」の項目50に対応しています。
今日のアジェンダ
- forEach()メソッド
- カスタム反復メソッド
カウント変数型のfor文では、条件式にさまざまなバグが潜みます。
for (let i = 0; i <= x.length; i++) { } //一回余計に繰り返す
for (let i = 1; i < x.length; i++) { } //最初の回が抜けている
for (let i = x; i >= 0; i--) { } //最初の回を繰り返す
for (let i = x - 1; i > 0; i--) { } //最初の回が抜けている
今回はこの、カウント変数型を使わない、というお話です。
forEach()メソッド
カウント変数型のfor文で書いたサンプルコードはこちらです。
function myFunction7_50_01() {
const players = [{ name: 'Tom', score: 100 }, { name: 'Bob', score: 200 }, { name: 'Ivy', score: 300 }];
for (let i = 0; i < players.length; i++) {
players[i].score++;
//同コード
// players[i].score = players[i].score + 1;
}
console.log(players); //[{ name:'Tom',score:101}, {name:'Bob',score:201}, {name:'Ivy',score:301}]
}
反復可能な配列などのオブジェクトを、カウント変数型のfor文を使わずに処理するため、forEach()メソッドがあります。
function myFunction7_50_02() {
const players = [{ name: 'Tom', score: 100 }, { name: 'Bob', score: 200 }, { name: 'Ivy', score: 300 }];
// players.forEach(function (p){
// p.score++;
// });
//アロー関数で書きましょう
players.forEach(player => player.score++);
console.log(players);//[{ name:'Tom',score:101}, {name:'Bob',score:201}, {name:'Ivy',score:301}]
}
空の配列タイプ
同様に、空の配列を用意して、条件にあう要素だけをPushする、というアプローチもあります。
function myFunction7_50_03() {
const input = [' Tom ', ' Bob ', ' John'];
const trimmed = [];
for (let i = 0; i < input.length; i++) {
trimmed.push(input[i].trim());
}
console.log(trimmed); //[ 'Tom', 'Bob', 'John' ]
}
もちろん、この処理も、forEach()メソッドで置き換えできます。
function myFunction7_50_04() {
const input = [' Tom ', ' Bob ', ' John'];
const trimmed = [];
input.forEach(name => trimmed.push(name.trim()));
console.log(trimmed); //[ 'Tom', 'Bob', 'John' ]
}
map()メソッド
ES5から登場したmap()メソッドであれば、さらに簡潔に書くことができます。
function myFunction7_50_05() {
const input = [' Tom ', ' Bob ', ' John'];
const trimmed = input.map(name => name.trim());
console.log(trimmed);//[ 'Tom', 'Bob', 'John' ]
}
filter()メソッド
同様に、コールバック関数として、filter()メソッドが提供されています。
カウント変数型のfor文を見かけたときは、ほぼmap()やfilter()やreduce()メソッドで置き換えできると思って間違いありません。
function myFunction7_50_06() {
const listings = [{ price: 1 }, { price: 2 }, { price: 4 }];
const min = 3;
const max = 5;
const result = listings.filter(listing => listing.price >= min && listing.price <= max);
console.log(result); // [ { price: 4 } ]
}
カスタム反復メソッド
書籍では、オリジナルのカスタム反復メソッドを書いてみよう、というテクニックが紹介されていました。
たとえば、このように、配列の要素が10未満の、最長の配列を返す関数です。
function myFunction7_50_07() {
/**
* 配列の要素が10未満の、最長配列を返す関数
* @param {Array} 整数の1次元配列
* @param {Function} 引数で渡された整数が条件に合えばbool値をreturnする関数
* @return {Array} 結果の2次元配列
*/
function takeWhile(a, pred) {
const result = [];
for (let i = 0; i < a.length; i++) {
if (!pred(a[i])) break;
result[i] = a[i];
}
return result;
}
const prefix = takeWhile([1, 2, 4, 8, 16, 32], function (n) { return n < 10; });
console.log(prefix); //[ 1, 2, 4, 8 ]
return prefix;
}
このカスタム関数は、プロトタイプに仕込ませて、モンキーパッチ(項目42参照)することができます。
function myFunction7_50_08() {
Array.prototype.takeWhile = function (pred) {
const result = [];
for (let i = 0; i < this.length; i++) {
if (!pred(this[i])) break;
result[i] = this[i];
}
return result;
};
const prefix = [1, 2, 4, 8, 16, 32].takeWhile(function (n) { return n < 10; });
console.log(prefix); //[ 1, 2, 4, 8 ]
}
カスタム反復メソッドの落とし穴
では、この関数をカウント変数型から、forEach()メソッドに置き換えできないものでしょうか。
結果は、途中でループを止める処理がむずかしそうです。
function myFunction7_50_09() {
function takeWhile(a, pred) {
const result = [];
a.forEach((element, index) => {
if (!pred(element)) { /* ?終了命令はどうする? */ return }
result[index] = element;
});
return result;
}
const prefix = takeWhile([1, 2, 11, 4, 8, 16, 32], function (n) { return n < 10; });
console.log(prefix); // [ 1, 2, , 4, 8 ]
return prefix;
}
some()メソッドとevery()メソッド
個人的にあまり使いどころのなかった、組み込みArrayオブジェクトの、2つのメソッドがあります。
- Array.some()・・・配列の要素いずれかが条件にあてはまる/ないならtrue/false
- Array.every()・・・配列の要素すべてが条件にあてはまる/ないならtrue/false
function myFunction7_50_10() {
const some = [1, 10, 100].some(element => element > 5); //true
console.log(some);
const every = [1, 10, 100].every(element => element > 5); //false
console.log(every);
}
このメソッドの仕様を生かして、さきほどのカスタム反復メソッドを、forEach()メソッドで書き換えてみましょう。
ループ処理の中で、処理を中断するbreakと、次の処理にスキップするcontinueが実装できました。
function myFunction7_50_11() {
function takeWhile(a, pred) {
const result = [];
a.every((element, index) => {
if (!pred(element)) return false; //breakと同じ意味
result[index] = element;
return true; //continueと同じ意味
});
return result;
}
const prefix = takeWhile([1, 2, 4, 8, 16, 32], function (n) { return n < 10; });
console.log(prefix); // [ 1, 2, 4, 8 ]
return prefix;
}
GAS中級講座を受けられた方は、ぜひmap()メソッドやfilter()メソッドなどのコールバック関数を習得しましょう。
講座ではあまり取り上げないのですが、個人的に、もっともコスパのいいメソッドの部類に入ると思っています。
5日間でmapとfilterを習得できる中級講座ブートキャンプもSlackにご用意しました。がんばりましょう!
まとめ
以上で、複数のループよりも反復メソッドが好ましい、をお届けしました。
次回は、「「配列のようなオブジェクト」にも、総称的な配列メソッドを再利用できる」 をお届けします。