背景
Array.fromとSpread構文は、配列やオブジェクトを新しい配列へ変換する際によく使用される機能である。
かつてはInternet ExplorerがArray.fromやSpread構文をサポートしておらず、Babelなどでトランスパイルが必要だった。しかし、モダンブラウザはこれらをネイティブでサポートしており、実装方法を簡略化できる。
これらの機能は使用場面が重複することも多く、どちらを選択すべきか判断が難しい場合もある。まずはパフォーマンスの観点から両者を比較することで、データ量や処理内容に応じた使い分けの基準を得ることができる。
Array.from とは
Array.from() は、以下のものから Array を生成します。
- 反復可能オブジェクト(
Map や Set のような要素を取得するオブジェクト) - オブジェクトが反復可能でない場合は、配列風オブジェクト(
length プロパティおよび添字の付いた要素を持つオブジェクト)
Array.from() - JavaScript | MDNdeveloper.mozilla.org
コード例
// 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つずつ適用する必要がある場合に使用することができます。
スプレッド構文 (...) - JavaScript | MDNdeveloper.mozilla.org
コード例
// 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];
ベンチマーク結果
ChromeとFirefoxではそれぞれのパフォーマンスがほぼ同等である一方で、SafariではSpread構文の方が良い結果となった。
イミュータブルなオブジェクト
// 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];
ベンチマーク結果
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);
ベンチマーク結果
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はデータの取得と変換を1回の走査で完了できるが、Spread構文を使用して.mapのような変換する場合、2回の走査が必要になる。
結論
単なる変換であればどちらを利用しても問題はなさそうだった。可読性やコードの簡潔さを重視する場合はSpread構文を、処理効率を重視する場合はArray.fromを選択すると良いだろう。ただし、実際のパフォーマンスはブラウザの実装やデータの特性によって大きく異なる可能性があるため、パフォーマンスが重要となる処理では、実際の使用環境でベンチマークテストを実施する必要がある。