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

スポンサーリンク
rectangle

Array.forEachとは

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

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

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


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

// forEachを利用した記法
arr.forEach(function(value, index){
    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++;
    }

 このソースコードを見ればわかるとおり、配列の全要素に対して、単純にコールバックを呼び出しているだけですから、途中でループを抜けることはできません。どうしてもbreak、returnさせたい場合は、for文をそのまま使うか、あるいは代替処理を行うにあたって少し処理を見直す必要があります。

各処理の代替処理

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

breakしたい

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

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


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

someを使う

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

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

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

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

someを使うことは妥当か?

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

 しかし、メソッド名はsomeです。直訳すると、「いくつかの」という意味ですから、「配列のいくつかの要素に対して処理を実行する」と考えれば、妥当でない、とは必ずしも言い切れません。

 find関数やfindIndex関数も同じような動きをしますが、そのふたつの関数よりはこちらを使ったほうがいくらか好ましいとは感じます。

continueしたい

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

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

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

// forEach
arr.forEach(function(value, index){
    if(value === "c"){
        return;
    }
    console.log(value);
});

returnしたい

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

 無理やりやるとしても、以下のようなコードになります。

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


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

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

if(shouldReturn){
    return;
}

 こんなことをするくらいなら、素直にfor文べた書きでも構わないと思います。

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

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

線形探索プログラム

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

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

var expectId = 1;

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

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

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

var expectId = 1;

return arr.find(function(value){
    return value.id === expectId;
});

配列検査プログラム

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

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

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

var containsText = "l";

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

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

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

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

var containsText = "l";

return arr.every(function(item){
    return (item.name.indexOf(containsText) > -1);
});

まとめ

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

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

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