JavaScriptのArray.forEachをbreak、continue、returnさせたい

Array.forEachとは

 ECMA-262標準で追加されたメソッドで、for文を書かなくても、Arrayの要素すべてに対してcallbackの処理を行ってくれる便利なメソッドです。

 たとえば、配列の中身をすべて表示するようなコードを組んでみましょう。

// テストデータ
const TEST_DATA = ["a", "b", "c", "d", "e"];


// 従来の記法
for(let i = 0; i < TEST_DATA.length; i++){
  console.log(TEST_DATA[i]);
}

// forEachを利用した記法
TEST_DATA.forEach(value => {
  console.log(value);
});

 関数型言語っぽい非常にシンプルな記述が可能ですね。

forブロック内における、break、continue、returnと同じ動作をforEachにさせることは可能か?

 不可能です。forEachの実装は以下のとおりです。

 MDN web docsのArray.prototype.forEach()の「互換性」項目を参照してください。33行目から55行目までが実際のループ処理部分です。

    // 7. Repeat, while k < len
    while( k < len ) {

      var kValue;

      // a. Let Pk be ToString(k).
      //   This is implicit for LHS operands of the in operator
      // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
      //   This step can be combined with c
      // c. If kPresent is true, then

      if ( k in O ) {

        // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
        kValue = O[ k ];

        // ii. Call the Call internal method of callback with T as the this value and
        // argument list containing kValue, k, and O.
        callback.call( T, kValue, k, O );
      }
      // d. Increase k by 1.
      k++;
    }

 このソースコードを見ればわかるとおり、配列の全要素に対して、単純にコールバック関数(forEach関数の第一引数に渡したfunction)を呼び出しているだけですから、途中でループを抜けることはできません。

 例えば、以下のようなことは出来ません。

arr.forEach(value => {
  if(value === 'continue'){
    continue;
    // Uncaught SyntaxError: Illegal continue statement: no surrounding iteration statementが発生。
    // for-loop内ではなく、ただの関数ブロックなのでcontinueは不可能
  }

  if(value === 'break'){
    break;
    // Uncaught SyntaxError: Illegal break statementが発生。
    // continueと同様に、for-loop内ではなく、ただの関数ブロックなので不可能
  }

  if(value === 'return'){
    return;
    // 次のループに移ってしまう(for文でのcontinueと同じ動き)
  }

  console.log(value);
});

 どうしてもbreak、returnさせたい場合は、for文をそのまま使うか、あるいは代替処理を行うにあたって少し処理を見直す必要があります。

各処理の代替処理

 break、returnに代わる処理を考えてみることにします。従来のfor文をそのまま使ってもよいのですが、せっかく関数型インターフェースによって簡潔な処理を書くことが可能となっていますので、もう少し考えてみましょう。

breakしたい

 では、”c”という要素に当たった時点で即座にbreakするプログラムを想定してみましょう。

// テストデータ
const TEST_DATA = ["a", "b", "c", "d", "e"];

// 従来の記法
for(let i = 0; i < TEST_DATA.length; i++){
  // "c"の要素にあたったら、繰り返し処理を終了し、forブロックを抜ける
  if(TEST_DATA[i] === "c"){
    break;
  }
  console.log(TEST_DATA[i]);
}

実行結果

a
b

someを使う

 Array.prototype.some()という関数があります。MDNドキュメントによれば、これはテスト関数なのですが、コールバック関数がtrueを返すと即座にtrueを返すという特性を利用して、以下のようにすることができます。

// テストデータ
const TEST_DATA = ["a", "b", "c", "d", "e"];

// Array.prototype.some()
TEST_DATA.some(function(value, index){
    if(value === "c"){
        return true;
    }
    console.log(value);
});

 実行結果

a
b

 想定どおりの結果が得られました。

someを使うことは妥当か?

 さて、someは先に述べたように、テスト関数として定義されています。配列の中身を走査し、条件に合致する要素があればtrueを返し、なければfalseを返す関数ですから、他に影響を与えるような(console.logや、別の変数を変えるような)ことを、callbackの中でやるべきではない、というのが共通認識だと思います。

 他にも同じような動き(trueを返すとその時点で繰り返しを打ち切る)をする関数(indexOf など)がありますが、こちらも本来の使い方ではないため、素直にfor-ofとか、for-inを使うほうがベターです。

// テストデータ
const TEST_DATA = ["a", "b", "c", "d", "e"];

for(let value of TEST_DATA){
  if(value === "c"){
    break;
  }
  console.log(value);
}

 実行結果

a
b

continueしたい

 これは、continueの代わりにreturnするだけです。コールバック関数はreturnされた時点で次のループに処理を渡しますから、return句を書いてあげればcontinueとまったく同じ意味になります。

// テストデータ
const TEST_DATA = ["a", "b", "c", "d", "e"];

// 従来の記法
for(let i = 0; i < TEST_DATA.length; i++){
  if(TEST_DATA[i] === "c"){
    continue;
  }
  console.log(TEST_DATA[i]);
}

// forEach
TEST_DATA.forEach(value => {
  if(value === "c"){
    return;
  }
  console.log(value);
});

returnしたい

 ご存じのように、関数ブロックから一気に抜ける命令がreturn句ですが、そもそもforEach、あるいはその他の関数型インターフェースに渡すコールバックの値は、関数です。つまり、forEachコールバック関数から抜けることはできても、その上位の呼び出し元からもreturnすることは言語仕様的に不可能です。

 無理やりやるとしても、以下のようなコードになりますが、複雑化するため非推奨です。

// テストデータ
const TEST_DATA = ["a", "b", "c", "d", "e"];


// 従来の記法
for(let i = 0; i < TEST_DATA.length; i++){
  if(TEST_DATA[i] === "c"){
     return;
  }
  console.log(TEST_DATA[i]);
}

// Array.prototype.some()
const shouldReturn = TEST_DATA.some(function(value, index){
  if(value === "c"){
    return true;
  }
  console.log(value);
});

if(shouldReturn){
  return;
}

 ほぼ確実にPull Requestで指摘されて直す羽目になるので、絶対に書かないようにしましょう。for-ofや、for-inを利用してください。


// for-in for(let i in TEST_DATA){ if(TEST_DATA[i] === "c"){ return; } console.log(TEST_DATA[i]); } // for-of for(let value of TEST_DATA){ if(value === "c"){ return; } console.log(value); }

そもそもの関数設計を見直す

 仕様によっては、どうしてもbreak、returnしなければならない処理を書かなければいけないこともありますが、そういったことは大変稀です。

線形探索プログラム

 たとえば、単純な線形探索プログラムを書いてみます。配列を最初から探っていって、該当するものがあれば即座にそのオブジェクトを返す、というメソッドです。

// テストデータ
const TEST_DATA = [
  {id: 0, name: "alpha"},
  {id: 1, name: "beta"},
  {id: 2, name: "charlie"}
];

const EXPECTED_ID = 1;

// 従来の記法
for(let i = 0; i < TEST_DATA.length; i++){
  if(TEST_DATA[i].id === EXPECTED_ID){
    return TEST_DATA[i];
  }
}

 これはforEachやsomeメソッドを使うべきではありません。より適切なメソッドのArray.prototyoe.find()が用意されています。次のように書くべきでしょう。

// テストデータ
const TEST_DATA = [
  {id: 0, name: "alpha"},
  {id: 1, name: "beta"},
  {id: 2, name: "charlie"}
];

const EXPECTED_ID = 1;

return TEST_DATA.find(function(value){
    return value.id === EXPECTED_ID;
});

配列検査プログラム

 あるいは、配列の中身を順次検査し、想定のデータがあるかをテストする関数が必要かもしれません。

 オブジェクトのnameに「l」という文字が存在する場合はtrueを返す関数を書いてみましょう。

// テストデータ
const TEST_DATA = [
  {id: 0, name: "alpha"},
  {id: 1, name: "beta"},
  {id: 2, name: "charlie"}
];

const EXPECTED_CONTAINS_TEXT = "l";

// 従来の記法
for(var i = 0; i < TEST_DATA.length; i++){
  if(TEST_DATA[i].name.indexOf(EXPECTED_CONTAINS_TEXT) > -1){
    return TEST_DATA[i];
  }
}

// someを利用
return TEST_DATA.some(function(item){
  return (item.name.indexOf(EXPECTED_CONTAINS_TEXT) > -1);
});

 もしかすると、「すべての要素に含まれているか」を検査しないといけないかもしれません。しかし安心してください。Array.prototype.every()という関数が用意されています。

// テストデータ
const TEST_DATA = [
  {id: 0, name: "alpha"},
  {id: 1, name: "beta"},
  {id: 2, name: "charlie"}
];

const EXPECTED_CONTAINS_TEXT = "l";

return TEST_DATA.every(function(item){
  return (item.name.indexOf(EXPECTED_CONTAINS_TEXT) > -1);
});

まとめ

 一連の処理には、必ず適した記法というものが存在します。しかし、その記法は時代の変遷によって常に移り変わるものです。もし、旧来の古臭いfor文が処理の概要に適している場合に、「古臭いから」という考えでわざわざ新しい記法を採用し、複雑なロジックを形成することは至極馬鹿馬鹿しい話です。

 しかし、より適した記法が用意されているにも関わらず、従来の記法にとらわれてしまうのも良くないことです。我々SE、PGは、常に効率化を考える生き物ですから、よりよい方法があれば、それを採択することは自然な流れです。

 「それがその場に適しているか?」を常に考えながら、効率のよいコーディングを行いたいものですね。

コメント

  1. […] JavaScript の Array.forEach を break、continue、return させたい | Deep Rain […]

  2. 通りすがり より:

    例外を上げて try/catch で捕捉るればいいのでは?

  3. tam-tam より:

    コメントありがとうございます!

    もちろんそういったやり方もありますが、何も知らないメンバーがコードを読んだときに、例外の意味をなかなか把握しづらいことが予想されます。

    コードは「動かすためのもの」でもありますが、「読むためのもの」の側面も持ち合わせているので、後から読んだときに迷わないほうがベターなのです。

    もしも、例外を上げてtry/catchで捕捉するやり方のほうがすべてのメンバーにとって読みやすい場合は、そちらの方が良いと思います。プロジェクトやチームの状況を見て柔軟に採択してみてください!

  4. 匿名 より:

    > 通りすがり
    > 例外を上げて try/catch で捕捉るればいいのでは?

    素直にforを使うより見苦しく効率の悪い手段ですね。