配列に配列を追加するベストプラクティス

投稿者: Anonymous
var a = [1, 2, 3, 4];
var b = [5, 6, 7];
for (var x of b) {
    a.push(x);
}
// a = [1, 2, 3, 4, 5, 6, 7]

配列に配列を追加する方法は他に Array.prototype.push.apply やライブラリを使用した方法などもあると思いますが、どの方法が一番良いと思いますか?
処理速度、メモリ使用量、書きやすさなど、メリットも含め教えてください。

また、スプレッド構文を使用した

a.push(...b);

という書き方もあるようですが、これは有効な方法なのでしょうか。

2019/04/13 追記
Array.prototype.concat を使用する、という回答を頂きましたが、これは「配列に他の配列や値をつないで新しい配列を返す」というもののようです。私が求めてるものはそうではなく、既存の配列に要素を追加するものです。

a = a.concat(b)

と書けば結果は同じなのかも知れませんが、Array.prototype.concat は新しい配列を生成しますので、配列のサイズが非常に大きい場合などに(一時的な)メモリ使用量の増加や、処理速度の面でデメリットがあるのではないかと感じています。そのようなことはないのでしょうか?

解決

結論からいえば、場合によるがfor-of文とpushを使うのが無難です。特に、配列への要素の追加を1回行うだけならconcatのほうが速い場合がありますが(Google Chromeで顕著)、要素の追加が何度も繰り返される場合、質問者さんが懸念する通り、concatを使うと配列オブジェクトが何度も作られるという点で不利になります。


まず、配列への要素の追加1回のパフォーマンスを比較してみます。この回答では4つの方法(for-ofループ、Array.prototype.push.apply、spread構文、そしてArray.prototype.concat)を比較してみました。

まずjsbench.me1000要素の場合100万要素の場合で速度を比較すると手元のPC(Win10 + Google Chrome)では以下の表のようになり、concatが最速という結果になりました。配列の要素として数値以外にも文字列やオブジェクトなどを試しましたが同じ傾向でした。また、Edgeも同じ傾向でした。

|               | 1000 items    | 1000000 items |
| ------------- | ------------- | ------------- |
| for-of        | 59,383 ops/s  | 17 ops/s      |
| push & apply  | 78,932 ops/s  | error         |
| push & spread | 19,079 ops/s  | error         |
| concat        | 104,070 ops/s | 40 ops/s      |

さらに、上記の表では100万要素の場合にa.push.apply(a, b)a.push(...b)でエラーが発生しています。その理由は、これらの処理はあくまで「pushを100万個の引数で呼ぶ」という処理に相当するため、処理系が対応している引数の数の上限を超えているからです。この2つをこの目的で使用するのは(bの要素数が少なくてエラーが起きないと確信できない限りは)やめたほうがよいでしょう。

一方、Firefoxでは下記の結果になります。先程とは傾向がかなり異なり、concatよりもfor-ofのほうが速いです(それでもChromeの1/3以下の性能ですが)。

|               | 1000 items   | 1000000 items |
| ------------- | ------------ | ------------- |
| for-of        | 18,815 ops/s | 8 ops/s       |
| push & apply  | 3,750 ops/s  | error         |
| push & spread | 4,231 ops/s  | error         |
| concat        | 3,663 ops/s  | 5 ops/s       |

というわけで、for-ofconcatの二択ではありますが一概にどちらがベストとは言いにくい結果となりました。


次に、テストケースを100要素の追加を1000回行うように変更してみます。この場合、concatでは中間オブジェクトが多く作られるので不利になると予想されます。

このテストケースをGoogle Chromeで試してみたところ以下の結果となりました(Firefoxも同じ傾向)。

| for-of | 388 ops/s |
| concat | 4 ops/s   |

また、データは省略しますがconcatのほうがメモリ使用量が上がっており、これが実行速度にも悪影響を与えていると考えられます。


まとめると、concatはGoogle Chromeでは最も高速に動作しますが、何度も繰り返し使用する場合は新しいオブジェクトが何度も生成されることが原因で性能が大きく低下します。一方、for-ofはそのような性能悪化が発生しない上、Firefoxでは最も高速です。このことから、for-ofが最も無難ではないかと考えられます。

シチュエーションにもよりますが、配列への複数要素の追加があまり頻繁に起こらないならどちらでもよいでしょう。concatのほうが何をやっているのか分かりやすくてよいかもしれません。しかし、この操作が頻繁に行われる場合にはconcatを避けたほうがよいでしょう。

回答者: Anonymous

Leave a Reply

Your email address will not be published. Required fields are marked *