Iteratorに対してmapやfilterのようなヘルパー関数を作用させる
Baseline 2025でIterator Helpersが導入され、Iteratorオブジェクトにmapやfilterなどの便利な操作が直接適用可能になりました。これにより、配列への変換が不要になり、コードの可読性向上と遅延評価によるパフォーマンス改善が期待できます。
はじめに
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
オブジェクトに対して、map
・filter
・take
・drop
・flatMap
・reduce
・toArray
・forEach
・some
・every
・find
という操作と静的メソッドfrom
の利用が可能になりました。
Iteratorオブジェクト
Iterator
オブジェクトは繰り返し可能なデータの並びを処理するオブジェクトです。
next
という操作で次のデータを表すvalue
と後続のデータがないことを示すdone
を返します。
Iterator
オブジェクトは、他の多くの組み込みイテレータの基盤となる存在です。
Array.prototype[Symbol.iterator]
やMap.prototype.values
、Set.prototype.values
、Intl.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
ループを使って自前で煩雑な処理を記述したり、配列に変換してから配列に実装されているメソッド(map
やfilter
など)を利用する方法がよく使われていました。
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()
を実行するとvalue
が2
になるまで、done
がfalse
のオブジェクトを返します。
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
は遅延評価されません。全ての要素を取り出しながら指定した関数を適用します。take
やdrop
で実行する個数を調整できます。
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
オブジェクトの利用が楽しみです。