1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【JS新機能】map や filter の進化版が追加された【Iterator Helpers】

Last updated at Posted at 2024-11-30

今時のJavaScripterなら必ず使っているであろうArray.prototype.mapArray.prototype.filterですが、この度これらと同等の機能を持つ関数がIteratorのメソッドとして追加されました。

2024年11月現在、Safariが非対応ですがそれ以外の主要ブラウザでは実装済みです。またNode.jsでも22.0.0から使えるようです。

新しいメソッド

  • Iterator.prototype.drop()
  • Iterator.prototype.every()
  • Iterator.prototype.filter()
  • Iterator.prototype.find()
  • Iterator.prototype.flatMap()
  • Iterator.prototype.forEach()
  • Iterator.prototype.map()
  • Iterator.prototype.reduce()
  • Iterator.prototype.some()
  • Iterator.prototype.take()
  • Iterator.prototype.toArray()

使い方

使い方はArrayの同名のメソッドと基本的に同じです。

function *foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
}

const newIter = foo().filter(x => x % 2).map(x => x * 3);

for(const x of newIter) {
  console.log(x); // 3, 9
}

drop()take()Arrayにはないですが、drop(limit: number)はイテレーターの最初limit個を捨ててそれ以降を返すイテレータを返す、take(limit: number)はイテレータの最初limit個のみを返し以降を捨てたイテレータを返す関数です。

const newIter = foo().drop(1).take(2);

for(const x of newIter) {
  console.log(x); // 2, 3
}

Arrayのメソッドと何が違うのか?

これまでのArray.prototype.map()Array.prototype.filter()Arrayのメソッドなので当然Arrayにしか使えませんでした。

// ✅ 以下はこれまでもできた
[1, 2, 3, 4].map(x => x + 1) // [1, 2, 3, 4]はArray

// ❌ 以下はこれまでできなかった
foo().filter(x => x % 2).map(x => x * 3) // foo()はイテレーター
new Map([['a', 42], ['b', 43]]).keys().map(x => 'foo ' + x) // Map.keys()の戻り値はイテレーター

そこでこれまでイテレーターでfilterやmapしたい時にはスプレッド演算子を使う([...xxx])などしてArrayに変換していました。

// こうやっていた
[...foo()].filter(x => x % 2).map(x => x * 3)
[...new Map([['a', 42], ['b', 43]]).keys()].map(x => 'foo ' + x)

これをイテレーターにもメソッドを生やすことでイテレーターにも直接使えるようにしたのが今回のメソッド達です。そしてArray型だった戻り値もイテレータになります。

// これからは以下のままで動く
const x = foo().filter(x => x % 2).map(x => x * 3); // xは3,9を順に返すイテレーター
const y = new Map([['a', 42], ['b', 43]]).keys().map(x => 'foo ' + x); // yはfoo a,foo bを順に返すイテレーター

戻り値がイテレーターなので上のxのようにイテレーターに対して例えばfiltermapを連続して適用したイテレーターを作るということもできます。

Arrayのメソッドと比べて何がいいのか?

これまでもイテレーターをArrayに変換すれば同じことができた訳ですが、今回追加されたイテレーターのメソッドを使うことでどんな良いことがあるんでしょうか?主要なメリットはメモリ使用量が減ることです。

Arrayのメソッドは配列を入力として配列を返します。よって例えば以下のコードでは入力配列[1, 2, 3, 4]から始まり、次にfilterを適用した後の配列[1, 3]が作られ、最後にmapを適用した配列[3, 9]が作られます。

[1, 2, 3, 4].filter(x => x % 2).map(x => x * 3)

この長さ4程度の配列なら問題ないですが、例えばこの配列の長さが10万あったらどうでしょうか?万単位の要素をもつ配列がfiltermapをかけるごとに毎回生成されるのはメモリ効率が良くありません。

一方でイテレーターを使った場合はどうなるのか。イテレーター版のfiltermapはそれらの関数を呼び出された時点では実際のフィルターやマップ処理は実行されません。いつ実行されるかというとそれら関数の戻り値のイテレーターが次の値を取得しようとした時です。

例えば以下のコードを使ってfor文でイテレーターから値を取り出すときの挙動を考えます。

const b = foo(); // bはイテレーターとする
const c = b.map(x => x * 3);
// const c = foo().map(x => x * 3); と同じ
for(const x of c) {
  console.log(x);
}

for文によりイテレーターcから次の値を取り出そうとすると、cの元であるmapが入力であるイテレーターbから次の値を取り出しその値に3をかけて返します。その値がfor文の変数xとして代入されconsole.logされることになります。ループ内の処理が終わるとfor文はまた次の値をcから取り出そうとし最初と同じことが起こる、これが次の値がなくなるまで繰り返されます。

更に以下のようにfiltermapを連続して適用した場合はどうなるのか、考え方は同じです。

const a = foo(); // aはイテレーターとする
const b = a.filter(x => x % 2);
const c = b.map(x => x * 3);
// const c = foo().filter(x => x % 2).map(x => x * 3); と同じ
for(const x of c) {
  console.log(x);
}

for文によりイテレーターcから次の値を取り出そうとすると、cの元であるmapが入力であるイテレーターbから次の値を取り出そうとします。するとbの元であるfilterは入力であるイテレーターaから次の値を取り出します。filterはもし取り出した値がフィルタ条件に合致しなかった場合はさらに次の値をaから取り出して、合致したものを返します。返された値はmapが取り出して3をかけて返します。これがxとなり、ループ内の処理が終わるとまた次の値をcから取り出そうとして最初に戻るという事が繰り返されます。

ここで注目すべきなのは、このイテレーターを使った処理ではその過程で配列を生成していないという点です。要素をストリーミングのようにひとつずつ処理していくので入力が10個でも10万個でも、filtermapでの加工に使うメモリ使用量は変わりません。

このメモリ使用量が要素数に関わらず一定であり、中間で無駄なメモリを消費しないのがイテレーターの優れた点です。

あとがき

JavaScriptのmapやfilterが配列にしか使えないことに私は以前から違和感を持っていました。C#とかPythonだとイテレーターに対してmapやfilterして遅延実行するのがむしろ普通だからです。それがJavaScriptでもようやくできるようになりました。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?