いまどきの配列操作 2024年版 第1回 新しく配列をつくるメソッド

今回は主にES2018からES2024までで追加された、比較的新しい配列操作メソッドのうち「新しく配列をつくるメソッド」について解説します。これまで煩雑だった処理がシンプルになるケースもあります。

発行

著者 宇野 陽太 フロントエンド・エンジニア
いまどきの配列操作 2024年版 シリーズの記事一覧

はじめに

このシリーズでは、主にES2018からES2024までで追加された、配列操作のメソッドを解説します。

配列操作は、プログラムを書く上で頻繁に出現する処理です。そのため、世の中には数多くのライブラリも存在します。

そういった中で、JavaScriptの標準機能としてどのような処理が可能なのかを知っておくことは、わかりやすく保守しやすいコードを書くためには非常に重要であると言えます。

このシリーズでは、大きく分けて次の内容で各メソッドを紹介します。

  • 新しく配列をつくるメソッド
  • 配列から別の配列をつくるメソッド
  • 配列を検索するメソッド
  • 既存メソッドを非破壊化したメソッド

この記事では、上記のメソッドのうち、「新しく配列をつくるメソッド」を紹介します。

また、過去にも配列操作を扱ったシリーズがあります。ES2018以前の仕様について知りたい方は、こちらも参考にしてみてください。

新しく配列をつくるメソッド

まずは、新しく配列をつくるメソッドArray.fromAsync()を紹介します。従来あった類似メソッドとの違いをおさえながら理解すると、より深く理解できるでしょう。

記事の後半では発展的なケーススタディとして、ページングされたデータを返すAPIから非同期でデータを取得する際、Array.fromAsync()がどのように機能するかを解説しています。

Array.fromAsync()の実装状況(2024.5.17追記)

記事公開当初、Array.fromAsync()はES2024で追加された仕様と紹介していましたが、現在はStage 3の仕様でした。訂正してお詫びいたします。執筆時点(2024年4月)では、ほとんどの主要ブラウザで利用可能で、Node.jsでも2024年4月25日にリリースされたv22から使用できるようになりました。

最新の実装状況については、MDNなどの各種サイトを参照してください。

オブジェクトから配列を生成する:Array.fromAsync()

fromAsync()は、反復可能オブジェクト、または非同期反復可能オブジェクトから、配列を生成する静的メソッドです。

補足:反復可能オブジェクト

反復可能オブジェクトというのは、繰り返し処理に対応できる仕組みを持ったオブジェクトのことです。コードでいうとfor...of文で処理できるオブジェクトです。たとえば、[a, b, c]というオブジェクトがあれば、aの次はbbの次はcを呼び出し、処理することができます。配列や文字列は典型的な反復可能オブジェクトです。

反復可能オブジェクトから配列を生成するというところは、Array.from()に似ていますが、fromAsync()は、配列で履行されるPromiseオブジェクトを返すという点と、非同期反復可能オブジェクトを受け付けるという点で異なります。

次の例を見てみましょう。

Promiseオブジェクトを持った反復可能オブジェクトから配列を生成

const iterable = new Set([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3),
]);

console.log(Array.fromAsync(iterable));
// => Promise
console.log(Array.from(iterable));
// => [Promise, Promise, Promise]

反復可能オブジェクトとして、要素にPromiseオブジェクトを持ったSetオブジェクトを用意しました。このSetオブジェクトを、fromAsync()に渡した場合、Promiseオブジェクトを返します。

一方で、from()に渡した場合は、Promiseオブジェクトを要素に持つ配列を返します。

Promiseの結果を取得する際に、この違いが影響してきます。結果を取得するコードを書いてみると、次のようになります。

fromAsyncとfrom()の違い

// Promiseを返すので…
Array.fromAsync(iterable).then(console.log);
// => [1, 2, 3]

// Promiseの配列を処理しないといけないので…
Array.from(iterable)
  .reduce(
    (p, el) => p.then((acc) => el.then((v) => [...acc, v])),
    Promise.resolve([]),
  )
  .then(console.log);
// => [1, 2, 3]

// あるいは…
Promise.all(iterable).then(console.log);

// もしくは…
const res = [];
for await (const v of iterable) res.push(v);
console.log(res);

fromAsync()の返り値は単一のPromiseオブジェクトです。このPromiseオブジェクトは、生成元のPromiseオブジェクトの結果をそれぞれ集約した配列を持っています。ですので、then()をつなげるだけで、結果を配列で取得することができます。

一方、from()の返り値はPromiseの配列です。そのため、同じような結果を得ようと思ったら、配列内の各Promiseが履行されるのを待つ処理を書く必要があります。サンプルコードでは3つの方法を例示していますが、どの処理においても各Promiseが履行されるのを待っています。

1つ目のコードではreduce()を使ってPromiseの配列を処理していますが……実際にはこんなわかりづらいコードを書くことはまずないでしょう。fromAsync()と同じように、反復可能オブジェクトからPromiseを返すPromise.all()を使うか、for await...ofを使うほうが代替案としてはよりシンプルです。

いずれにせよ、fromAsync()の登場によって、反復可能オブジェクトがPromiseオブジェクトを持つ場合であっても、より直感的かつ簡単に配列を生成することができるようになりました。

発展:非同期反復可能オブジェクトから配列を生成する

この節は、JavaScriptでの実装に慣れた人を想定しています。もしピンとこなくても、そういうものもあるのだ、とさらっと読み進めてください。

fromAsync()の特徴として、前述したとおり、非同期反復可能オブジェクトからでも配列を生成することができる、というものがあります。これはfrom()にはない機能です。

非同期反復可能オブジェクトとは、値がPromiseでラップされた、繰り返し処理が可能なオブジェクトのことです。一般的には連続したHTTPリクエストや、ストリーミングデータの処理に使われます。

今回は、ページングされたデータを返すAPIを想定して、非同期反復可能オブジェクトから配列を生成してみます。

まずは、APIリクエストを模した関数を定義します。

APIリクエスト関数

const fetchPageData = (page) =>
  Promise.resolve({
    data: `Page ${page} data.`,
    next: page < 3 ? page + 1 : null,
  });

console.log(await fetchPageData(0));
// => { data: 'Page 0 data.', next: 1 }

この関数は「ページングされたデータを、1ページずつ返すAPIにリクエストする」ことを想定した関数です。

各ページごとに異なるデータを返し、次のページの情報として、nextでページ番号を返します。4ページ分のデータを返したらnextnullを渡し、すべてのデータを返したものとします。

では、この関数を繰り返し実行してすべてのデータを取得するための、非同期ジェネレーター関数を定義します。

ジェネレーター関数とは、関数の処理の途中で中断・再開を可能にする仕組みです。ジェネレーター関数が返す値は、その特性上、反復可能オブジェクトとして扱うことができます。同様に、非同期ジェネレーター関数が返す値は、非同期反復可能オブジェクトとして扱うことができます。

ジェネレーター関数については、次の記事で詳しく解説しています。こちらも参考にしてみてください。

次の例を見てみましょう。

すべてのページを取得するジェネレーター関数

async function* fetchAllPages() {
  let page = 0;
  let hasMore = true;

  while (hasMore) {
    const res = await fetchPageData(page);
    page = res.next;
    if (res.next === null) hasMore = false;
    yield res.data;
  }
}

const generator = fetchAllPages();

console.log(await generator.next());
// => { value: 'Page 0 data.', done: false }
console.log(await generator.next());
// => { value: 'Page 1 data.', done: false }
console.log(await generator.next());
// => { value: 'Page 2 data.', done: false }
console.log(await generator.next());
// => { value: 'Page 3 data.', done: false }

fetchAllPages()は、先ほど定義したfetchPageData()を繰り返し実行する、非同期ジェネレーター関数です。fetchPageData()が返すPromiseが履行されるたびに処理を中断し、履行された値を返します。

fetchAllPages()が返す値は、非同期反復可能オブジェクトとして扱うことができるため、Array.fromAsync()を使って配列を生成できるというわけです。

fetchAllPages()をfromAsync()の引数に指定

async function* fetchAllPages() {
  // 省略
}

console.log(await Array.fromAsync(fetchAllPages()));
// [
//   'Page 0 data.',
//   'Page 1 data.',
//   'Page 2 data.',
//   'Page 3 data.'
// ]

// 従来の方法だと…
const result = [];
for await (const data of fetchAllPages()) {
  result.push(data);
}
console.log(result);

このように、Array.fromAsync()を使うことで、fetchAllPages()から非同期で取得できるすべての値を元に、配列を生成することができます。従来の方法であれば、for await...ofを使って、fetchAllPages()を反復処理して、その結果を配列へと詰める必要があります。

配列生成という点で見れば、Array.fromAsync()の登場によって、より直感的かつ簡潔に記述できるようになったと言えるでしょう。

ここまでのまとめ

今回は、ES2018以降に追加された配列操作メソッドのうち、

  • 新しく配列をつくるメソッド

について紹介しました。

次回は、配列から別の配列をつくるメソッド、配列を検索するメソッドについて紹介する予定です。