この文書の目的
タイトル通りですが、最近JavaScript(Node.js)において配列の結合でトラブルに遭遇したので、どうしてたら良かったのかを改めてまとめてみました。
条件
この文書では、配列の結合とは以下のようなものを指すこととします。
a=[1,2,3]; b=[10,20,30]
をc=[1,2,3,10,20,30]
のようにする。
この例ではたかだか3要素しかない2つの配列を結合するだけなので、まあ普通にconcat()
でも使いましょう、で終わりですが、やりたいこと(というかトラブルになったこと)は、「ある程度の要素数を持ったある程度の数の配列を結合すること」です。
以下に示すサンプルでは、要素として配列を持った親配列が以下のように定義されているものとします。
parentArray = [
[1,2,3,....],
[10,20,30,....],
[100,200,300,....],
...
]
これを結合して、
resultArray = [1,2,3,....,10,20,30,....,100,200,300,........]
となるようにするのが目的とします。
さあ比較しよう
普通の方法(普通に使えるけど条件によってはトラブルになるよ)
concat
配列を結合するんだからconcat
に決まってんだろ、と思う方もいるかもしれません。
後述するように使い方によってはconcat
は有効ですが、以下のような使い方は気を付ける必要があります。
let resultArray = [];
for (const subArray of parentArray) {
resultArray = resultArray.concat(subArray);
}
まず気を付けるべきは、concat
は「非破壊的関数だ」ということです。
従って、実行結果である「結合して大きくなった配列」を、ループの度にコピーしていることになります。
0要素の配列(resultArrayの初期値)と100要素の配列を足すと100要素のコピーが、その100要素の配列にまた100要素の配列を足すと200要素のコピーが発生します。これを1000要素になるまで繰り返すと、5500要素分のコピーが発生することになります。要素は1000しかないのに。
つまり、コピーに要する時間が指数的に伸びて行ってしまうのがこの方法です。
push
concat
と違い、push
は破壊的変更をおこなう関数ですのでコピーは足す分だけになるので速いです。ただし配列自体を引数として渡しても配列のまま足してくれちゃうので、引数リストに変換してあげる必要があります。これはスプレッド演算子を使って実現できます。
const resultArray = [];
for (const subArray of parentArray) {
resultArray.push(...subArray);
}
とても速いです。速いのですが唯一の欠点は、これが引数リストとして渡さないといけない、ということです。
引数リストの長さの制限は、配列長の制限よりはるかに厳しいです。従って、長大な配列に対してこれを使うと、スタックオーバーフローと言われてしまう可能性があります。
ちょっとひねった方法(トラブルを起こしにくい方法)
concat(その2)
そもそもparentArrayが用意されている時点でここにたどり着いてる人もいると思いますが、concat
には引数を複数渡すことができます。ですから、今回の例のようにparentArrayのような形で結合したい配列が用意されているのなら以下のようにするだけで結合した配列が出来上がります。
const resultArray = [].concat(...parentArray);
これは速いです。試した中では最速でした。
今回はparentArrayが最初から用意されていましたが、プログラム的に順次足していくような処理になってる場合だと見落とすかもしれませんね。この形に持っていければ高速化できるので意識してみるといいと思います。
欠点は、強いて言えばここでもスプレッド演算子で配列→引数リストの変換をおこなっていることですね。
引数リストの制限にひっかかるほどの数の配列を結合するケースはそんなにないだろうとは思いますけど・・・。
flat
concat
の引数リストの制限も許せない、というのであればこの方法も考えてみてください。
const resultArray = parentArray.flat();
まあまあ速いです。concat(その1)に比べたら雲泥の差で速いです。pushやconcat(その2)に対する利点は引数リストを使わないこと、です。
終わりに
参考になりましたら幸いです。