javascript - スプレッド演算子 vs array.concat()
スプレッド演算子とarray.concat()の違いは何ですか
let parts = ['four', 'five'];
let numbers = ['one', 'two', 'three'];
console.log([...numbers, ...parts]);Array.concat() 関数
let parts = ['four', 'five'];
let numbers = ['one', 'two', 'three'];
console.log(numbers.concat(parts));両方の結果は同じです。では、どのような状況でそれらを使いたいのでしょうか?そして、どちらがパフォーマンスに最適ですか?
回答 #1
concat とスプレッドは、引数が配列でない場合は非常に異なります。
引数が配列でない場合、concat はそれを丸ごと追加しますが、... はそれを反復しようとし、できなければ失敗します。次を考えてみてください:
a = [1, 2, 3]
x = 'hello';
console.log(a.concat(x)); // [ 1, 2, 3, 'hello' ]
console.log([...a, ...x]); // [ 1, 2, 3, 'h', 'e', 'l', 'l', 'o' ]
ここで、concat は文字列を原子的に扱いますが、... はデフォルトのイテレータを使い、文字ごとに処理します。
別の例:
x = 99;
console.log(a.concat(x)); // [1, 2, 3, 99]
console.log([...a, ...x]); // TypeError: x is not iterable
繰り返しになりますが、concat においては数値はアトムであり、... はそれをイテレートしようとして失敗します。
ついに:
function* gen() { yield *'abc' }
console.log(a.concat(gen())); // [ 1, 2, 3, Object [Generator] {} ]
console.log([...a, ...gen()]); // [ 1, 2, 3, 'a', 'b', 'c' ]
concat はジェネレーターを反復処理する試みをせず、そのまま全体を追加しますが、... はジェネレーターからすべての値をきれいに取得します。
要約すると、引数が配列でない可能性がある場合、concat と ... の選択は、それらを反復処理したいかどうかに依存します。
上記は concat のデフォルトの動作について説明していますが、ES6 では Symbol.isConcatSpreadable を使ってそれを上書きする方法が提供されています。デフォルトでは、このシンボルは配列に対して true、それ以外には false です。これを true に設定すると、concat は引数を反復処理するようになり、まるで ... のように動作します:
str = 'hello'
console.log([1,2,3].concat(str)) // [1,2,3, 'hello']
str = new String('hello');
str[Symbol.isConcatSpreadable] = true;
console.log([1,2,3].concat(str)) // [ 1, 2, 3, 'h', 'e', 'l', 'l', 'o' ]
パフォーマンスの観点では、concat の方が高速です。おそらく配列特有の最適化を活用できるためであり、一方で ... は共通のイテレーションプロトコルに従う必要があります。タイミング:
let big = (new Array(1e5)).fill(99);
let i, x;
console.time('concat-big');
for(i = 0; i < 1e2; i++) x = [].concat(big)
console.timeEnd('concat-big');
console.time('spread-big');
for(i = 0; i < 1e2; i++) x = [...big]
console.timeEnd('spread-big');
let a = (new Array(1e3)).fill(99);
let b = (new Array(1e3)).fill(99);
let c = (new Array(1e3)).fill(99);
let d = (new Array(1e3)).fill(99);
console.time('concat-many');
for(i = 0; i < 1e2; i++) x = [1,2,3].concat(a, b, c, d)
console.timeEnd('concat-many');
console.time('spread-many');
for(i = 0; i < 1e2; i++) x = [1,2,3, ...a, ...b, ...c, ...d]
console.timeEnd('spread-many');回答 #2
.concat() はより高速です。それ以外の場合、両方の引数がリストであれば同じように動作します。ここでは、各 1,000 万要素の配列 2 つを結合するのにかかる時間です(低い方が良いです):
| ブラウザ | [...a, ...b] | a.concat(b) |
|---|---|---|
| Chrome 113 | 350ミリ秒 | 30ミリ秒 |
| Firefox 113 | 400ミリ秒 | 63ミリ秒 |
| Safari 16.4 | 92ミリ秒 | 71ミリ秒 |
私は8GBのRAMを搭載したM1 MacBook Airでこのコードを実行しました:
const arraySize = 10000000;
const trials = 50;
const array1 = [];
const array2 = [];
for (let i = 0; i < arraySize; ++i) {
array1.push(i);
array2.push(i);
}
let spreadTime = 0;
for (let i = 0; i < trials; ++i) {
const start = performance.now();
const array3 = [...array1, ...array2];
const end = performance.now();
spreadTime += end - start;
}
let concatTime = 0;
for (let i = 0; i < trials; ++i) {
const start = performance.now();
const array3 = array1.concat(array2);
const end = performance.now();
concatTime += end - start;
}
// performance.now() returns milliseconds with a
// 5 microsecond resolution in isolated contexts and a
// 100 microsecond resolution in non-isolated contexts.
spreadTime = Math.round(spreadTime / trials * 1000) / 1000;
concatTime = Math.round(concatTime / trials * 1000) / 1000;
console.log(`${arraySize} items - spread: ${spreadTime}ms concat: ${concatTime}ms`);回答 #3
まあ、console.log(['one', 'two', 'three', 'four', 'five']) も同じ結果になるので、ここでどちらを使う必要があるのでしょうか? :P
一般的に、任意のソースから取得した2つ以上の配列がある場合はconcatを使用し、配列の一部として常に含まれる追加の要素が事前に分かっている場合は配列リテラルでスプレッド構文を使用します。ですので、コード内でconcatを使用した配列リテラルがある場合は、単にスプレッド構文を使い、それ以外の場合はconcatを使用します。
[...a, ...b] // bad :-(
a.concat(b) // good :-)
[x, y].concat(a) // bad :-(
[x, y, ...a] // good :-)
また、非配列の値を扱う場合、二つの代替案はかなり異なる動作をします。
回答 #4
私が有効だと思う唯一の違いは、大きな配列サイズにスプレッド演算子を使用すると、Maximum call stack size exceeded のエラーが発生する可能性があり、concat 演算子を使用することで回避できるという点です。
var someArray = new Array(600000);
var newArray = [];
var tempArray = [];
someArray.fill("foo");
try {
newArray.push(...someArray);
} catch (e) {
console.log("Using spread operator:", e.message)
}
tempArray = newArray.concat(someArray);
console.log("Using concat function:", tempArray.length)回答 #5
更新:
Concatは現在、常にspreadよりも高速です。次のベンチマークでは、小さな配列と大きな配列の両方が結合される様子が示されています: https://jsbench.me/nyla6xchf4/1
// preparation
const a = Array.from({length: 1000}).map((_, i)=>`${i}`);
const b = Array.from({length: 2000}).map((_, i)=>`${i}`);
const aSmall = ['a', 'b', 'c', 'd'];
const bSmall = ['e', 'f', 'g', 'h', 'i'];
const c = [...a, ...b];
// vs
const c = a.concat(b);
const c = [...aSmall, ...bSmall];
// vs
const c = aSmall.concat(bSmall)
前へ:
大きな配列に関してのパフォーマンスについては一部の回答が正しいですが、小さな配列を扱う場合のパフォーマンスはかなり異なります。
結果は https://jsperf.com/spread-vs-concat-size-agnostic で自分自身で確認できます。
ご覧の通り、小さい配列ではスプレッドの方が50%速く、一方で大きい配列ではconcatの方が何倍も速いです。
