【JavaScript】 Iterator(イテレーター)とは?避けて通りたいけど説明してみる
更新日:2024/03/13
JavaScriptにはイテレーターという機能があって、反復処理に使うと便利らしい。
別に知らなくても問題ないが…
今回は勉強がてら、イテレーターについて解説してみようと思う。
Iterator(イテレーター)とは?
JavaScriptでのイテレーターとは、次の4つの条件を満たすオブジェクトです。
条件2: next()を呼び出すたびに、順番に値を返す
条件3: 値は次のオブジェクトで返す
{ value: 値, done: false }
条件4: 返す値がない(終了した)場合、次のオブジェクトを返す
{ value: 戻り値, done: true }
(戻り値はイテレーターの実行結果です。なくても問題ありません。)
この条件を元に、簡単なイテレーターを作成してみます。
イテレーター例
const iteratorObj = {
count: 0,
next: function() {
this.count ++;
return ( this.count <= 5 ) ?
{ value: this.count * 2 , done: false }
: { done: true};
}
};
2から10までの偶数値をnext()を呼び出すたびに返すオブジェクトです。
このイテレーターは次のように使用します。
イテレーター使用例
const itr = iteratorObj.next();
while ( !itr.done ) {
console.log(itr.value); // 結果: 1回目 2
// 2回目 4
// 3回目 6
// 4回目 8
// 5回目 10
itr = a.next();
}
whileループでdoneの値をチェックしながら、value値を表示しています。
簡単ですね。
ですが実はこれが、イテレーターを理解するための大きなハードルです。
すなわち・・・
イテレーターって意味なくない?
理解しようとするモチベーションが湧かないです。
ループさせるだけなら、自分なりの実装でいいですもんね。
だから何なんだろう?
これだけなら、イテレーターは無駄な知識です。
しかしイテラブルなオブジェクトとして実装することで、便利に使用できるようになります。
iterable(イテラブル)とは
イテラブルなオブジェクトとは、イテレーターを作成して返す [Symbol.iterator]() というメソッドを持つオブジェクトです。
イテラブルなオブジェクト
先ほどのイテレーターを、そのままイテラブルにしてみます。
イテラブルなオブジェクト
const iteratorObj = {
[Symbol.iterator] : function(){ // [Symbol.iterator]() { と記述可能
return {
count:0,
next: function() {
this.count ++;
return ( this.count <= 5 ) ?
{ value: this.count * 2 , done:false }
: { done: true };
}
}
}
};
[Symbol.iterator]という名前のメソッド内で、新規でイテレータを作成してリターンしているだけです。
簡単ですね。
コンストラクターにイテラブルなオブジェクトを実装
コンストラクターはプロトタイプで[Symbol.iterator]を実装します。
コンストラクターはプロトタイプ
const iteratorConstructor = function(){};
iteratorObj.prototype[Symbol.iterator]=function(){
return {
count:0,
next: function() {
this.count ++;
return (this.count <= 5) ?
{value:this.count * 2 , done:false }
: {done:true};
}
}
};
クラスにイテラブルなオブジェクトを実装
クラスは次のようになります。
コンストラクターはプロトタイプ
const iteratorClass = class{
[Symbol.iterator](){
return {
count:0,
next: function() {
this.count ++;
return (this.count <= 5) ?
{value:this.count * 2 , done:false }
: {done:true};
}
}
};
}
イテラブルなオブジェクトの使い方
次にこのオブジェクトのどこが便利なのか、見ていきましょう。
for...of 文で値を取り出す
for of文を使うと、イテラブルなオブジェクトから値を一つ一つ取り出してくれます。
for of文で列挙
for( let val of iteratorObj ){
console.log(val);
}
スプレッド構文でArrayに変換
いつまでたっても、自作オブジェクトにforEachなどのArrayのメソッドを使用してエラーになる僕のようなおっちょこちょいさんは多いはず。
JavaScriptにはスプレッド構文というものがあって、イテラブルなオブジェクトを簡単にArrayに変換できます。
スプレッド構文でArrayに変換
const arr = [...iteratorObj];
arr.forEach( e => console.log( e ) );
また次のように、スプレッド構文で引数に指定することもできる。
スプレッド構文で引数に指定
const maxValue = Math.max(...iteratorObj); // Math.max( 2, 4, 6, 8, 10 )に展開される
console.log( maxValue ); // 10
■【JavaScript】 コード中の「...」は意味があった
一つのオブジェクトに複数のイテレーターを実装する
Arrayオブジェクトにはkeys()という、インデックスを反復するイテレーターを返すメソッドがあります。
メソッドで取得したイテレーターは for…of を使用できるので、イテラブルなオブジェクトともいえます。
Arrayオブジェクトはそのものがイテラブルなオブジェクトですが、その他にもイテラブルなオブジェクトを提供しているということです。
そこで自作のオブジェクトに、複数のイテラブルなオブジェクトをサポートしてみます。
オブジェクトプロパティ値を列挙
まずは内部データを列挙するイテレーターを実装してみます。
内部データを列挙するイテレーター
const iteratorCLass = class {
#data;
constructor(data){
this.#data = data;
}
[Symbol.iterator]() { // イテラブルなオブジェクト
return { // ①
count: -1,
value: Object.values( this.#data ),
next: this.#iterableNext // ②
}
};
#iterableNext(){
// thisは①
this.count ++;
return (this.count < this.value.length) ?
{value:this.value[this.count] , done:false }
: {done:true};
};
};
let itr = new iteratorCLass({data1:"a",data2:"b",data3:"c"});
for( let val of itr ){
console.log( val ); // 結果: "a" "b" "c"
}
プライベート変数を定義するために、class構文を使用しています。
next()内の処理(②)は後で紹介するコードと同じものなので、別関数にしてあります。
そのため少し分かりにくいですが、#iterableNext()のthis値は①のオブジェクトを想定しています。
オブジェクトに複数のイテレーターを定義
次に、オブジェクトに複数のイテレーターを実装してみます。
一つ目のイテレーターとは異なり、メソッドを実行してイテラブルなオブジェクトを取り出します。
次のコードは、前項(オブジェクトプロパティ値を列挙)のコードに keys()メソッドを追加したものです。
複数のイテレーター定義
const iteratorCLass = class {
・・・前項のコード
keys(){
return {
value:Object.keys( this.#data ),
next:this.#iterableNext,
[Symbol.iterator](){
return{
count:-1,
value:this.value,
next:this.next
};
}
};
};
};
const itr = new iteratorCLass({data1:"a",data2:"b",data3:"c"});
for( let val of itr.keys() ){
console.log( val ); // 結果: "data1" "data2" "data3"
}
iteratorCLassのkeys()は、内部データのキーを列挙するイテラブルなオブジェクトを返します。
返したオブジェクトのメソッドは this.#data を参照できないので、あらかじめオブジェクト内に値を確保しています。
ジェネレーターを使用したイテラブルなオブジェクト
最後に、これまでのコードをジェネレーターに書き換えてみます。
ジェネレーターについては、次の記事で解説しているので確認してみてください。
参考:【JavaScript】 Generator(ジェネレーター)ってなんだ?関数とは何が違うのか
ジェネレーターを使用したイテラブルなオブジェクト
const iteratorCLass = class {
#data;
constructor(data){
this.#data = data;
}
*[Symbol.iterator]() { // イテラブルなオブジェクト
yield* Object.values( this.#data );
};
keys(){
return {
value:Object.keys( this.#data ),
*[Symbol.iterator](){
yield* this.value;
}
};
};
};
const itr = new iteratorCLass({data1:"a",data2:"b",data3:"c"});
for( let val of itr ){
console.log( val ); // 結果: "a" "b" "c"
}
for( let val of itr.keys() ){
console.log( val ); // 結果: "data1" "data2" "data3"
}
コードが短くなりました。
今回のようなケースではジェネレーターを使わない理由は特にないので、積極的に使っていきましょう。
まとめ
僕の場合、今回の例のような取得時にデータ生成されるものを列挙するには、一度配列に格納してからforEachを利用していました。
今後は自作のイテレーターが活躍しそうですね。
■【JavaScript】 非同期イテレータと非同期ジェネレーターについて理解しよう
■【JavaScript】イテラブルな標準組み込みオブジェクト一覧
■【JavaScript】オブジェクトがイテラブルなオブジェクトかどうか判断する方法
更新日:2024/03/13
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。