k8o

Iteratorに対してmapやfilterのようなヘルパー関数を作用させる

公開: 2025年4月21日(月)
更新: 2025年4月21日(月)
閲覧数66 views

Baseline 2025でIterator Helpersが導入され、Iteratorオブジェクトにmapやfilterなどの便利な操作が直接適用可能になりました。これにより、配列への変換が不要になり、コードの可読性向上と遅延評価によるパフォーマンス改善が期待できます。

JavaScriptBaseline 2025Iterator Helpers

はじめに

Baseline 2025にIterator Helpersが追加されました。

GitHub - tc39/proposal-iterator-helpers: Methods for working with iterators in ECMAScript

Methods for working with iterators in ECMAScript. Contribute to tc39/proposal-iterator-helpers development by creating an account on GitHub.

github.com

具体的には、Iteratorオブジェクトに対して、mapfiltertakedropflatMapreducetoArrayforEachsomeeveryfindという操作と静的メソッドfromの利用が可能になりました。

Iteratorオブジェクト

Iteratorオブジェクトは繰り返し可能なデータの並びを処理するオブジェクトです。 nextという操作で次のデータを表すvalueと後続のデータがないことを示すdoneを返します。

Iteratorオブジェクトは、他の多くの組み込みイテレータの基盤となる存在です。 Array.prototype[Symbol.iterator]Map.prototype.valuesSet.prototype.valuesIntl.Segmenter.prototype.segment等の結果やNodeListなど、さまざまな反復処理はIteratorオブジェクトを継承しています。

const iterator = [1, 2, 3][Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

for (const str of 'hello'[Symbol.iterator]()) {
  console.log(str); // 'h' 'e' 'l' 'l' 'o'
}

Iterator Helpers追加の背景

これまでIteratorオブジェクトに対して何らかの処理を行いたい場合、for...ofループを使って自前で煩雑な処理を記述したり、配列に変換してから配列に実装されているメソッド(mapfilterなど)を利用する方法がよく使われていました。

const set = new Set([1, 2, 3]);
console.log([...set.values()].some((i) => i % 2 === 0)); // true

const segmenter = new Intl.Segmenter('ja', {
  granularity: 'grapheme',
});
[...segmenter.segment('あいうえお')].forEach((x) =>
  console.log(x.segment),
); // 'あ' 'い' 'う' 'え' 'お'

const nodeList = document.querySelectorAll('div');
Array.from(nodeList).forEach((x) => console.log(x.innerText)); // '...', '...'

上記のコードでは[...set.values()]のようにIteratorオブジェクトを配列に変換するような中間処理が挟まれるので非効率です。 また、配列に変換するタイミングで全てのデータを取り出すので、Iteratorオブジェクトが持つ呼び出されたタイミングにデータを取り出すという性質を生かせません。

Baseline 2025で追加されたIteratorオブジェクトに対する便利な操作を使えば、これらの問題を解決できます。

const set = new Set([1, 2, 3]);
console.log(set.values().some((i) => i % 2 === 0)); // true

const segmenter = new Intl.Segmenter('ja', {
  granularity: 'grapheme',
});
segmenter
  .segment('あいうえお')
  [Symbol.iterator]()
  .forEach((x) => console.log(x.segment)); // 'あ' 'い' 'う' 'え' 'お'

const nodeList = document.querySelectorAll('div');
nodeList[Symbol.iterator]().forEach((x) => console.log(x.innerText)); // '...', '...'

コード上で宣言的で読みやすい([Symbol.iterator]()は見慣れないかもですが)ことに加えて、Iteratorオブジェクトのまま扱えるのでsome等の処理が遅延評価されるようになります(forEachはされません)。 これにより、必要な分のデータだけが評価されるので、パフォーマンスの面でも有利です。

できること

nextメソッドを実行すると0から3までの値を返すIteratorオブジェクトのような値counterについて考えます。

const counter = {
  count: 0,
  next() {
    if (this.count < 3) {
      return { value: this.count++, done: false };
    }
    return { value: undefined, done: true };
  },
};

counter.next()を実行するとvalue2になるまで、donefalseのオブジェクトを返します。

console.log(counter.next()); // { value: 0, done: false }
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: undefined, done: true }

from

fromは、Iteratorオブジェクトの静的メソッドです。引数に渡した反復可能なオブジェクトから新しいIteratorオブジェクトを生成します。

これにより[Symbol.iterator]()を経由してnextメソッドを呼び出せるようになります。

const counterIter = Iterator.from(counter);
console.log(counterIter[Symbol.iterator]().next()); // { value: 0, done: false }
console.log(counterIter[Symbol.iterator]().next()); // { value: 1, done: false }
console.log(counterIter[Symbol.iterator]().next()); // { value: 2, done: false }
console.log(counterIter[Symbol.iterator]().next()); // { value: undefined, done: true }

以後紹介するメソッドはこの変換を通すことで利用可能になります(Set.prototype.valuesのようなIteratorオブジェクトを継承したオブジェクトで実行する場合は変換不要です)。

map

mapは、Iteratorオブジェクトの各要素に対して指定した関数を適用し、新しいIteratorオブジェクトを返します。

const counterIter = Iterator.from(counter);
const mappedIter = counterIter.map((x) => x * 2);
console.log(mappedIter.next()); // { value: 0, done: false }
console.log(mappedIter.next()); // { value: 2, done: false }
console.log(mappedIter.next()); // { value: 4, done: false }
console.log(mappedIter.next()); // { value: undefined, done: true }

mapを実行したタイミングではなく、next()を呼び出すタイミングで指定した関数の計算が行われています。

本来map中の副作用は避けるべきですが、遅延評価をわかりやすくするためにconsole.logを入れると、評価されるタイミングがわかります。

const counterIter = Iterator.from(counter);
const mappedIter = counterIter.map((x) => {
  console.log(x);
  return x * 2;
});
console.log(mappedIter.next()); // 0 { value: 0, done: false }
console.log(mappedIter.next()); // 1 { value: 2, done: false }
console.log(mappedIter.next()); // 2 { value: 4, done: false }

今後紹介する操作のほとんどはこのように遅延評価が行われます。遅延評価の仕組みが分かりにくい関数や遅延評価しない関数についてのみこの仕様について言及します。

filter

filterは、Iteratorオブジェクトの各要素に対して指定した関数を適用し、真となる要素だけを含む新しいIteratorオブジェクトを返します。

const counterIter = Iterator.from(counter);
const filteredIter = counterIter.filter((x) => x % 2 === 0);
console.log(filteredIter.next()); // { value: 0, done: false }
console.log(filteredIter.next()); // { value: 2, done: false }
console.log(filteredIter.next()); // { value: undefined, done: true }

filterも遅延評価されます。偽だった場合は真になるまで値を取り出し続けます。

const counterIter = Iterator.from(counter);
const filteredIter = counterIter.filter((x) => {
  console.log(x);
  return x % 2 === 0;
});
console.log(filteredIter.next()); // 0 { value: 0, done: false }
console.log(filteredIter.next()); // 1 2 { value: 2, done: false }
console.log(filteredIter.next()); // undefined { value: undefined, done: true }

take

takeは、Iteratorオブジェクトから指定した数の要素を取り出し、新しいIteratorオブジェクトを返します。

const counterIter = Iterator.from(counter);
const takenIter = counterIter.take(2);
console.log(takenIter.next()); // { value: 0, done: false }
console.log(takenIter.next()); // { value: 1, done: false }
console.log(takenIter.next()); // { value: undefined, done: true }

drop

dropは、Iteratorオブジェクトから指定した数の要素をスキップし、新しいIteratorオブジェクトを返します。

const counterIter = Iterator.from(counter);
const droppedIter = counterIter.drop(2);
console.log(droppedIter.next()); // { value: 2, done: false }
console.log(droppedIter.next()); // { value: undefined, done: true }

flatMap

flatMapは、Iteratorオブジェクトの各要素に対して指定した関数を適用し、得られた値をフラットにした新しいIteratorオブジェクトを返します。

const counterIter = Iterator.from(counter);
const flatMappedIter = counterIter.flatMap((x) => [x, x * 2]);
console.log(flatMappedIter.next()); // { value: 0, done: false }
console.log(flatMappedIter.next()); // { value: 0, done: false }
console.log(flatMappedIter.next()); // { value: 1, done: false }
console.log(flatMappedIter.next()); // { value: 2, done: false }
console.log(flatMappedIter.next()); // { value: 2, done: false }
console.log(flatMappedIter.next()); // { value: 4, done: false }
console.log(flatMappedIter.next()); // { value: undefined, done: true }

配列を返した場合、nextで配列をそのまま返すのではなく、配列の要素を1つずつ返します。

reduce

reduceは、Iteratorオブジェクトの各要素に対して指定した関数を適用し、最終的な値を返します。

const counterIter = Iterator.from(counter);
const reducedValue = counterIter.reduce((acc, x) => {
  return acc + x;
}, 0);
console.log(reducedValue); // 3

reduceは遅延評価されません。全ての要素を取り出しながら指定した関数を適用します。takedropで実行する個数を調整できます。

toArray

toArrayは、Iteratorオブジェクトの全ての要素を配列に変換します。

const counterIter = Iterator.from(counter);
const array = counterIter.toArray();
console.log(array); // [ 0, 1, 2 ]

toArrayも遅延評価されません。全ての要素を取り出しながら配列に変換します。

forEach

forEachは、Iteratorオブジェクトの各要素に対して指定した関数を適用します。

const counterIter = Iterator.from(counter);
counterIter.forEach((x) => {
  console.log(x);
});
// 0
// 1
// 2

forEachも遅延評価されません。全ての要素を取り出しながら指定した関数を作用させます。

some

someは、Iteratorオブジェクトの各要素に対して指定した関数を適用し、真となる要素が1つでもあればtrueを返します。

const counterIter = Iterator.from(counter);
const hasOdd = counterIter.some((x) => x % 2 === 0);
console.log(hasOdd); // true

someは遅延評価されます。真となる要素が見つかるまで値を取り出し続け、見つからない場合にfalseを返します。

const counterIter = Iterator.from(counter);
const hasOdd = counterIter.some((x) => {
  console.log(x);
  return x % 2 === 1;
});
console.log(hasOdd); // 0 1 true

every

everyは、Iteratorオブジェクトの各要素に対して指定した関数を適用し、全ての要素が真であればtrueを返します。

const counterIter = Iterator.from(counter);
const allEven = counterIter.every((x) => x % 2 === 0);
console.log(allEven); // false

everyは遅延評価されます。偽となる要素が見つかるまで値を取り出し続けます。

const counterIter = Iterator.from(counter);
const allEven = counterIter.every((x) => {
  console.log(x);
  return x % 2 === 0;
});
console.log(allEven); // 0 1 false

find

findは、Iteratorオブジェクトの各要素に対して指定した関数を適用し、真となる要素を返します。

const counterIter = Iterator.from(counter);
const found = counterIter.find((x) => x === 1);
console.log(found); // 1

findは遅延評価されます。真となる要素が見つかるまで値を取り出し続けます。

const counterIter = Iterator.from(counter);
const found = counterIter.find((x) => {
  console.log(x);
  return x === 1;
});
console.log(found); // 0 1 1

終わりに

Iteratorオブジェクトに追加された便利な操作について紹介しました。

これまでは、for...ofを使って操作を記述したり、配列に変換してから便利な操作を利用したりしていましたが、これからはIteratorオブジェクトに対して直接便利な操作を記述できるようになります。 コードがシンプルになり可読性が上がるだけではなく、Iteratorオブジェクトのまま扱えることで遅延評価の恩恵を受けられるのでパフォーマンスの面でも有効です。

これからのIteratorオブジェクトの利用が楽しみです。

この記事はどうでしたか?

500文字以内でご記入ください