[EffectiveJavaScript輪読会7]複数のループよりも反復メソッドが好ましい

GAS

どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。

前回のおさらい

前回は、「列挙の実行中にオブジェクトを変更しない」をお届けしました。

[EffectiveJavaScript輪読会7]配列の反復処理には、for...inループではなく、forループを使おう
どうも。つじけ(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にご用意しました。がんばりましょう!

まとめ

以上で、複数のループよりも反復メソッドが好ましい、をお届けしました。

次回は、「「配列のようなオブジェクト」にも、総称的な配列メソッドを再利用できる」 をお届けします。

参考資料

このシリーズの目次

タイトルとURLをコピーしました