[JavaScript] Array.fromとSpread構文はどちらを使うべきか

14 min read

背景

Array.fromとSpread構文は、配列やオブジェクトを新しい配列へ変換する際によく使用される機能である。

かつてはInternet ExplorerがArray.fromやSpread構文をサポートしておらず、Babelなどでトランスパイルが必要だった。しかし、モダンブルはこれらをネイティブでサポートしており、実装方法を簡略化できるようになった。

これらの機能は使用場面が重複することも多く、どちらを選択すべきか判断が難しい場合もある。まずはパフォーマンスの観点から両者を比較することで、適切な使い分けの指針を得ることができる。

Array.from とは

Array.from() は、以下のものから Array を生成します。

  • 反復可能オブジェクト(MapSet のような要素を取得するオブジェクト)
  • オブジェクトが反復可能でない場合は、配列風オブジェクト(length プロパティおよび添字の付いた要素を持つオブジェクト)

developer.mozilla.orgArray.from() - JavaScript | MDNArray.from() 静的メソッドは、反復可能オブジェクトや配列風オブジェクトからシャローコピーされた、新しい Array インスタンスを生成します。

コード例
// 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つずつ適用する必要がある場合に使用することができます。

developer.mozilla.orgスプレッド構文 - JavaScript | MDNスプレッド (...) 構文を使うと、配列式や文字列などの反復可能オブジェクトを、0 個以上の引数(関数呼び出しの場合)や要素(配列リテラルの場合)を目的の場所に展開することができます。オブジェクトリテラルでは、スプレッド構文によりオブジェクトのプロパティを列挙し、作成するオブジェクトにキーと値の組を追加します。

コード例
// 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.fromSpread
Chrome 1314550.9 Ops/sec4561.1 Ops/sec
Firefox 133213.2 Ops/sec231.2 Ops/sec
Safari 18248.6 Ops/sec1315.9 Ops/sec

ChromeとFirefoxではそれぞれのパフォーマンスがほぼ同等である一方で、SafariではSpread構文の方が良い結果となった。

Note

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.fromSpread
Chrome 131826.5 Ops/sec819.7 Ops/sec
Firefox 133179.2 Ops/sec92.7 Ops/sec
Safari 18147.8 Ops/sec138.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.fromSpread
Chrome 13159.0 Ops/sec147.8 Ops/sec
Firefox 133154.3 Ops/sec108.6 Ops/sec
Safari 1880.7 Ops/sec400.6 Ops/sec

Spread構文と.mapの組み合わせは配列を2回走査する必要があるため、1回の走査で完了するArray.fromの方が効率的なはずである。しかし、Safariでの測定結果は予想に反しており、.mapメソッドが内部的に最適化されている可能性がある。ただし、ベンチマークの実施方法やサンプルデータの特性による影響も考慮すべきであり、これが一貫した性能向上を意味するかは別途検証が必要である。


選択の指針

これらの結果を加味すると以下のように用途に応じてこれらを使い分けることが望ましい。

  • Array.fromの利点:
    • データ量が多い場合や変換処理が重い場合、走査が1回で済むため処理効率が良い
    • 特にマッピング処理を含む場合に効率的
  • Spread構文の利点:
    • 配列の展開だけを行いたい場合や後続の操作が不要な場合に直感的で簡潔に記述できる(コードの可読性や簡潔さを重視する場合)

ただし、Array.fromとSpread構文には、内部的な動作の違いによって走査の回数や効率に差があり、それがパフォーマンスに影響を与える可能性がある。

Array.fromの動作

Array.fromは以下のステップで動作する。

  1. 反復可能オブジェクトまたは配列風オブジェクトを取得:
    • 入力が反復可能オブジェクト(例: SetMap)の場合、iteratorプロトコルに基づいてその要素を1つずつ取得する
    • 入力が反復可能でない配列風オブジェクト(例: arguments)の場合、lengthプロパティをもとに要素を取得する
  2. マッピング関数(省略可能)の適用:
    • 第2引数としてマッピング関数が指定されている場合、取得した各要素に対してその関数を適用する
    • この処理は同時に行われるため、結果的に走査は1回で完了する

:

const arrFrom = Array.from([1, 2, 3], (x) => x * 2);
// 内部的に以下を実行:
// - [1, 2, 3] を1回走査
// - 各要素に (x) => x * 2 を適用
// => [2, 4, 6]

Spread構文の動作

Spread構文は以下のステップで動作する。

  1. 配列または反復可能オブジェクトを展開:
    • 入力が反復可能オブジェクトの場合、iteratorプロトコルに基づき、要素を1つずつ取得し、新しい配列にコピーする
    • この時点で走査が1回行われる
  2. 追加の処理(必要に応じて):
    • .mapなどのメソッドを適用する場合、配列の展開後に別途走査する

:

const arrSpread = [...[1, 2, 3]].map((x) => x * 2);
// 内部的に以下を実行:
// - [1, 2, 3] を展開(1回目の走査)
// - 展開後の配列に .map((x) => x * 2) を適用(2回目の走査)
// => [2, 4, 6]

構造的な違い

走査回数の比較

操作Array.fromSpread構文
配列・オブジェクトの取得1回1回
追加処理(例: .mapの適用)同時実行別途実行

結論として、Array.fromはデータの取得と変換を1回の走査で完了できるが、Spread構文を使用して.mapのような変換する場合、2回の走査が必要になる。

結論

単なる変換であればどちらを利用しても問題はなさそうだった。可読性やコードの簡潔さを重視する場合はSpread構文を、処理効率を重視する場合はArray.fromを選択すると良いだろう。ただし、実際のパフォーマンスはブラウザの実装やデータの特性によって大きく異なる可能性があるため、パフォーマンスが重要となる処理では、実際の使用環境でベンチマークテストを実施する必要がある。