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
で書こうとすると以下のように書けます。
これを reduce()
で実現する方法を考えます。
Array.prototype.reduce()
の構文
まず reduce()
の構文を確認しましょう。長くなりますが、MDN からそのまま引用します。
引数
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
として返す最初の値がなければ)エラーが発生します。返値
配列全体にわたって「縮小」コールバック関数を実行した結果の値です。
ちょっとわかりづらいかもしれませんのでコールバック関数の引数も踏まえて構文を表すと以下のようになります。
(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 にしてしまえば以降のイテレーションは実行されないのではと考え以下のように実装してみます。
少しややこしいですが、合計値は各イテレーションで使用する必要があるのと、最終的に reduce()
の返値として break 時のインデックスが必要なので、それぞれをプロパティとして持つオブジェクトとして accumulator
にしています。
accumulator
にオブジェクトを使用するの結構好きで こちらの記事 とかでも使っています。
条件を満たすと元配列 arr
を splice(1)
でインデックス 1 以降の要素を削除して以降のイテレーションが実行されないようにしています。
Array.prototype.splice()
は 2 番目と 3 番目の引数を省略することで、1 番目の引数で与えられた配列位置以降の要素を全て削除することができます。
引数
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()
してみます。
期待通りになりました。
あとがき
Array.prototype.reduce()
で条件を満たすと break する方法を紹介しました。何を reduce したいかにもよりますが、正直可読性はいまいちなので、while
とかでもよい気がしました。どうしても宣言的にしたかったり、むやみにループ外スコープに変数を増やしたくないケースでは使えるのかなと思いました。