[JavaScript] Array.fromとSpread構文はどちらを使うべきか
背景
Array.from
とSpread構文は、配列やオブジェクトを新しい配列へ変換する際によく使用される機能である。
かつてはInternet ExplorerがArray.from
やSpread構文をサポートしておらず、Babelなどでトランスパイルが必要だった。しかし、モダンブルはこれらをネイティブでサポートしており、実装方法を簡略化できるようになった。
これらの機能は使用場面が重複することも多く、どちらを選択すべきか判断が難しい場合もある。まずはパフォーマンスの観点から両者を比較することで、適切な使い分けの指針を得ることができる。
Array.from とは
Array.from()
は、以下のものからArray
を生成します。
- 反復可能オブジェクト(
Map
やSet
のような要素を取得するオブジェクト)- オブジェクトが反復可能でない場合は、配列風オブジェクト(
length
プロパティおよび添字の付いた要素を持つオブジェクト)
コード例
// 1. 文字列から配列を作成
const str = 'hello';
Array.from(str); // ['h', 'e', 'l', 'l', 'o']
// 2. DOM NodeListを配列に変換
const divs = document.querySelectorAll('div');
const divsArray = Array.from(divs); // DOMノードの配列に変換
// 3. 連番の数値配列を生成
Array.from({ length: 5 }, (_, i) => i + 1); // [1, 2, 3, 4, 5]
// 4. Mapオブジェクトのキーまたは値を配列に変換
const map = new Map([
['a', 1],
['b', 2],
['c', 3],
]);
Array.from(map.keys()); // ['a', 'b', 'c']
Array.from(map.values()); // [1, 2, 3]
// 5. 関数の引数(arguments)を配列に変換
function example() {
return Array.from(arguments);
}
example(1, 2, 3); // [1, 2, 3]
// 6. 重複を除去しつつ配列化
Array.from(new Set([1, 2, 2, 3, 3, 4])); // [1, 2, 3, 4]
Spread構文とは
スプレッド構文は、オブジェクトまたは配列の要素をすべて新しい配列またはオブジェクトに含める必要がある場合、または関数呼び出しの引数リストに1つずつ適用する必要がある場合に使用することができます。
コード例
// 1. 配列の結合
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// 2. 配列の途中に要素を挿入
const numbers = [1, 2, 5, 6];
const withInserted = [1, 2, ...['3', '4'], 5, 6]; // [1, 2, '3', '4', 5, 6]
// 3. 関数の引数として展開
function sum(x, y, z) {
return x + y + z;
}
const numbers = [1, 2, 3];
sum(...numbers); // 6
// 4. 文字列を文字の配列に分割
const chars = [...'hello']; // ['h', 'e', 'l', 'l', 'o']
// 5. オブジェクトのプロパティを展開
const defaults = { theme: 'dark', lang: 'en' };
const userConfig = { lang: 'ja' };
const merged = { ...defaults, ...userConfig }; // { theme: 'dark', lang: 'ja' }
// 6. 配列の複製と要素の追加
const original = [1, 2, 3];
const copy = [...original, 4]; // [1, 2, 3, 4]
// 7. Rest parametersとの組み合わせ
function example(first, ...rest) {
return [first, rest];
}
const numbers = [1, 2, 3, 4];
example(...numbers); // [1, [2, 3, 4]]
ベンチマーク
以下のシーンでそれぞれのパフォーマンスを計測する。
- 大量の配列
- イミュータブルなオブジェクト
- マッピング(
.map()
)
Array.from
とスプレッド構文は、いずれも配列の要素数(n)
に比例して処理時間が増加する O(n)
の計算量を持つ。この性質を実証するため、データ量は100万件のデータを用いて実行速度を測定する。
計測はMeasureThat.netを利用した。
大量の配列
// 配列を作成
const largeArray = [];
for (let i = 0; i < 1_000_000; i++) {
largeArray.push(i);
}
// => [0, 1, 2, 3, 4, ..., 999999]
Array.from
const arrayFrom = Array.from(largeArray);
Spread
const arraySpread = [...largeArray];
ベンチマーク結果
Array.from | Spread | |
---|---|---|
Chrome 131 | 4550.9 Ops/sec | 4561.1 Ops/sec |
Firefox 133 | 213.2 Ops/sec | 231.2 Ops/sec |
Safari 18 | 248.6 Ops/sec | 1315.9 Ops/sec |
ChromeとFirefoxではそれぞれのパフォーマンスがほぼ同等である一方で、SafariではSpread構文の方が良い結果となった。
Ops/sec (Operations per second) は、1秒間に実行可能な操作(処理)の回数を表す指標であり、特定のコードや処理のパフォーマンスを測る際に用いられる。処理が1秒間に何回実行できるかを示す数値である。
イミュータブルなオブジェクト
// Setオブジェクトを作成
const largeSet = new Set();
for (let i = 0; i < 1_000_000; i++) {
largeSet.add(i);
}
// => Set {0, 1, 2, 3, 4, ..., 999999}
Array.from
const arrayFromSet = Array.from(largeSet);
Spread
const arraySpreadSet = [...largeSet];
ベンチマーク結果
Array.from | Spread | |
---|---|---|
Chrome 131 | 826.5 Ops/sec | 819.7 Ops/sec |
Firefox 133 | 179.2 Ops/sec | 92.7 Ops/sec |
Safari 18 | 147.8 Ops/sec | 138.6 Ops/sec |
Chromeの結果では、Array.from
と Spread構文のパフォーマンスにほとんど差が見られなかった。いくつかの測定で結果のばらつきが見られたものの、両者の性能はほぼ同等と考えられる。
マッピング
// 配列を作成
const largeArray = [];
for (let i = 0; i < 1_000_000; i++) {
largeArray.push(i);
}
// => [0, 1, 2, 3, 4, ..., 999999]
Array.from
const arrFromMapped = Array.from(largeArray, (x) => x * 2);
Spread
const arrSpreadMapped = [...largeArray].map((x) => x * 2);
ベンチマーク結果
Array.from | Spread | |
---|---|---|
Chrome 131 | 59.0 Ops/sec | 147.8 Ops/sec |
Firefox 133 | 154.3 Ops/sec | 108.6 Ops/sec |
Safari 18 | 80.7 Ops/sec | 400.6 Ops/sec |
Spread構文と.map
の組み合わせは配列を2回走査する必要があるため、1回の走査で完了するArray.from
の方が効率的なはずである。しかし、Safariでの測定結果は予想に反しており、.map
メソッドが内部的に最適化されている可能性がある。ただし、ベンチマークの実施方法やサンプルデータの特性による影響も考慮すべきであり、これが一貫した性能向上を意味するかは別途検証が必要である。
選択の指針
これらの結果を加味すると以下のように用途に応じてこれらを使い分けることが望ましい。
Array.from
の利点:- データ量が多い場合や変換処理が重い場合、走査が1回で済むため処理効率が良い
- 特にマッピング処理を含む場合に効率的
- Spread構文の利点:
- 配列の展開だけを行いたい場合や後続の操作が不要な場合に直感的で簡潔に記述できる(コードの可読性や簡潔さを重視する場合)
ただし、Array.from
とSpread構文には、内部的な動作の違いによって走査の回数や効率に差があり、それがパフォーマンスに影響を与える可能性がある。
Array.from
の動作
Array.from
は以下のステップで動作する。
- 反復可能オブジェクトまたは配列風オブジェクトを取得:
- 入力が反復可能オブジェクト(例:
Set
やMap
)の場合、iterator
プロトコルに基づいてその要素を1つずつ取得する - 入力が反復可能でない配列風オブジェクト(例:
arguments
)の場合、length
プロパティをもとに要素を取得する
- 入力が反復可能オブジェクト(例:
- マッピング関数(省略可能)の適用:
- 第2引数としてマッピング関数が指定されている場合、取得した各要素に対してその関数を適用する
- この処理は同時に行われるため、結果的に走査は1回で完了する
例:
const arrFrom = Array.from([1, 2, 3], (x) => x * 2);
// 内部的に以下を実行:
// - [1, 2, 3] を1回走査
// - 各要素に (x) => x * 2 を適用
// => [2, 4, 6]
Spread構文の動作
Spread構文は以下のステップで動作する。
- 配列または反復可能オブジェクトを展開:
- 入力が反復可能オブジェクトの場合、
iterator
プロトコルに基づき、要素を1つずつ取得し、新しい配列にコピーする - この時点で走査が1回行われる
- 入力が反復可能オブジェクトの場合、
- 追加の処理(必要に応じて):
.map
などのメソッドを適用する場合、配列の展開後に別途走査する
例:
const arrSpread = [...[1, 2, 3]].map((x) => x * 2);
// 内部的に以下を実行:
// - [1, 2, 3] を展開(1回目の走査)
// - 展開後の配列に .map((x) => x * 2) を適用(2回目の走査)
// => [2, 4, 6]
構造的な違い
走査回数の比較
操作 | Array.from | Spread構文 |
---|---|---|
配列・オブジェクトの取得 | 1回 | 1回 |
追加処理(例: .map の適用) | 同時実行 | 別途実行 |
結論として、Array.from
はデータの取得と変換を1回の走査で完了できるが、Spread構文を使用して.map
のような変換する場合、2回の走査が必要になる。
結論
単なる変換であればどちらを利用しても問題はなさそうだった。可読性やコードの簡潔さを重視する場合はSpread構文を、処理効率を重視する場合はArray.from
を選択すると良いだろう。ただし、実際のパフォーマンスはブラウザの実装やデータの特性によって大きく異なる可能性があるため、パフォーマンスが重要となる処理では、実際の使用環境でベンチマークテストを実施する必要がある。