JavaScript reduce() のループを途中で抜ける(break)

JavaScript の Array.prototype.reduce() で途中で条件を満たしたら break する方法を考えてみました。

やりたいこと

JavaScript の Array.prototype.reduce() で途中で条件を満たしたらループを中断して途中で抜ける(for 文における breakwhile 文的なこと)方法を考えてみました。

例えば [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 として返す最初の値がなければ)エラーが発生します。

返値

配列全体にわたって「縮小」コールバック関数を実行した結果の値です。

Array.prototype.reduce() - JavaScript | MDN

ちょっとわかりづらいかもしれませんのでコールバック関数の引数も踏まえて構文を表すと以下のようになります。

reduce((accumulator, currentValue, currentIndex, array) => {}, initialValue);

(accumulator, currentValue, currentIndex, array) => {}callbackFn にあたります。この callbackFn の返値が次のイテレーションで accumulator の値になります。currentValue には reduce() を実行している配列(array)の現在イテレーションの値が入ります。 accumulator の初期値は initialValue で与えることができ、initialValue が与えられなかった場合は、array[0] が初期値として使用され、currentValuearray[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 にオブジェクトを使用するの結構好きで こちらの記事 とかでも使っています。

条件を満たすと元配列 arrsplice(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.prototype.splice() - JavaScript | MDN

なお、ここで元配列の参照に大元の 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 とかでもよい気がしました。どうしても宣言的にしたかったり、むやみにループ外スコープに変数を増やしたくないケースでは使えるのかなと思いました。