JavaScript reduce() のループを途中で抜ける(break)
JavaScript の Array.prototype.reduce() で途中で条件を満たしたら break する方法を考えてみました。
やりたいこと
JavaScript の Array.prototype.reduce() で途中で条件を満たしたらループを中断して途中で抜ける(for 文における break や while 文的なこと)方法を考えてみました。
例えば [1, 2, 3, 4, 5] という配列があって、配列の最初の要素から順番に足していく場合に合計が初めて 10 以上となる配列要素位置(インデックス)を知りたいとします。(すごい簡単な例なので 1 + 2 + 3 + 4 = 10 となり求めたいインデックスは 3 であることがすぐわかりますが…)
while で書こうとすると以下のように書けます。
const array = [1, 2, 3, 4, 5];
let sum = 0,
idx = -1;
while (sum < 10) {
sum += array[++idx];
}
console.log(sum, idx); // 10, 3
これを reduce() で実現する方法を考えます。
Array.prototype.reduce() の構文
まず reduce() の構文を確認しましょう。長くなりますが、MDN からそのまま引用します。
reduce(callbackFn); reduce(callbackFn, initialValue);引数
callbackFn
配列の各要素に対して実行される関数です。その返値は、次にcallbackFnを呼び出す際のaccumulator引数の値になります。最後の呼び出しでは、返値はreduce()の返値となります。この関数は以下の引数で呼び出されます。
accumulator
前回のcallbackFnの呼び出し結果の値です。初回の呼び出しではinitialValueが指定されていた場合はその値、そうでない場合はarray[0]の値です。currentValue
現在の要素の値です。初回の呼び出しではinitialValueが指定された場合はarray[0]の値であり、そうでない場合はarray[1]の値です。currentIndex
currentValueの位置です。初回の呼び出しでは、initialValueが指定された場合は0、そうでない場合は1です。array
reduce()が呼び出された配列です。initialValue省略可
コールバックが最初に呼び出された時にaccumulatorが初期化される値です。initialValueが指定された場合、callbackFnは配列の最初の値をcurrentValueとして実行を開始します。 もしinitialValueが指定されなかった場合、accumulatorは配列の最初の値に初期化され、callbackFnは配列の 2 つ目の値をcurrentValueとして実行を開始します。この場合、配列が空であれば(accumulatorとして返す最初の値がなければ)エラーが発生します。返値
配列全体にわたって「縮小」コールバック関数を実行した結果の値です。
ちょっとわかりづらいかもしれませんのでコールバック関数の引数も踏まえて構文を表すと以下のようになります。
reduce((accumulator, currentValue, currentIndex, array) => {}, initialValue);(accumulator, currentValue, currentIndex, array) => {} が callbackFn にあたります。この callbackFn の返値が次のイテレーションで accumulator の値になります。currentValue には reduce() を実行している配列(array)の現在イテレーションの値が入ります。 accumulator の初期値は initialValue で与えることができ、initialValue が与えられなかった場合は、array[0] が初期値として使用され、currentValue は array[1] から始まるので注意が必要です。
Array.prototype.reduce() で break してみる
前項の callbackFn の 4 番目の引数には元配列が与えられます。条件を満たしたら元配列の配列長を 1 にしてしまえば以降のイテレーションは実行されないのではと考え以下のように実装してみます。
const array = [1, 2, 3, 4, 5];
const { summary, index } = array.reduce(
({ summary, index }, item, idx, arr) => { // callbackFn
if (summary + item >= 10) arr.splice(1);
return { summary: summary + item, index: idx };
},
{ summary: 0, index: 0 } // initialValue
);
console.log(summary, index, array); // 10, 3, [1]
少しややこしいですが、合計値は各イテレーションで使用する必要があるのと、最終的に reduce() の返値として break 時のインデックスが必要なので、それぞれをプロパティとして持つオブジェクトとして accumulator にしています。
accumulator にオブジェクトを使用するの結構好きで こちらの記事 とかでも使っています。
条件を満たすと元配列 arr を splice(1) でインデックス 1 以降の要素を削除して以降のイテレーションが実行されないようにしています。
Array.prototype.splice() は 2 番目と 3 番目の引数を省略することで、1 番目の引数で与えられた配列位置以降の要素を全て削除することができます。
splice(start, deleteCount, item1, item2, /* …, */ itemN);引数
start
配列の変更を始める位置のゼロから始まるインデックスで、整数に変換されます。
(中略)
deleteCount省略可
配列のstartの位置から取り除く古い要素の個数を示す整数です。
deleteCountが省略された場合、またはdeleteCountの値がstartで指定した位置より後の要素数以上の場合、startから配列の末尾までのすべての要素が削除されます。
(中略)
item1, …,itemN省略可
配列に追加する要素で、startから始まります。 要素を指定しなかった場合、splice()は単に配列から要素を取り除きます。
なお、ここで元配列の参照に大元の array ではなく、callbackFn 引数の arr を使用している理由は reduce() の前に何らかの処理がされていてメソッドチェーンで reduce() をつなぐケースを想定しているため、callbackFn 引数の arr を使用しています。例えば array.filter(...).reduce(...) のように元配列から filter() で抽出した上で、reduce() を行うようなケースでは、filter()で抽出した配列を参照するにはcallbackFn引数のarr` を使用する他ありません。
さて、上記実装では summary, index は想定通りとなりましたが、元配列が変更されてしまうという問題があります。
そこで、slice() を使用し、元配列をコピーした新しい配列を作成した上で reduce() してみます。
const array = [1, 2, 3, 4, 5];
const { summary, index } = array.slice(0).reduce(
({ summary, index }, item, idx, arr) => { // callbackFn
if (summary + item >= 10) arr.splice(1);
return { summary: summary + item, index: idx };
},
{ summary: 0, index: 0 } // initialValue
);
console.log(summary, index, array); // 10, 3, [1, 2, 3, 4, 5]
期待通りになりました。
あとがき
Array.prototype.reduce() で条件を満たすと break する方法を紹介しました。何を reduce したいかにもよりますが、正直可読性はいまいちなので、while とかでもよい気がしました。どうしても宣言的にしたかったり、むやみにループ外スコープに変数を増やしたくないケースでは使えるのかなと思いました。