JavaScript入門 反復処理イテレータの解説
ES6で追加された、for…of文の挙動に関わるイテレータを解説しています。
イテレータの記法
イテレータとは、for…of文などを使ったオブジェクト(MapやSet、または独自で定義したオブジェクトなど)の「反復処理の挙動を定義するときに使うオブジェクト」です。
構文:イテレータの構造
const iterator = {
next(){ ※1
return { ※2
done: [ true | false ],※3
value: 値 ※4
}
}
}
イテレータの特徴
- ※1イテレータ(オブジェクト)には、必ずnextメソッドが格納されている必要がある
- ※2nextメソッドの戻り値は、value、doneを保持するオブジェクトである必要がある
- ※3doneは、反復の終了をtrueまたはfalseでnextメソッドの実行元に知らせる。trueの場合には、反復処理が終了したことを表す。
- ※valueには、反復ごとに取得したい値を設定する
数値を返すイテレータ
次のコードのgetInterator関数は、イテレータを戻り値として返す関数です。この関数から返されたイテレータは、nextメソッドの実行ごとに1ずつインクリメントされた値を返す。
1ずつインクリメントした値を返すイテレータ
//イテレータを生成する関数
function getIterator( max ){
let value = 0; //※1
return {
next(){
if( value < max ){ //valueがmaxより小さいとき
return {
done: false, //doneがfalseの場合は反復処理の継続を表す
value: value++ //※1のvalueを値として設定してから+1行う
}
} else {
return {
done: true //doneがtrueになると、反復処理の終了を表す。
} //また、このときイテレータが返す値(value)
} //は一般的に使用しないため、valueプロパティは省略している
}
}
}
const iterator = getIterator( 3 );
console.log( iterator.next() );
>{ done: false, value:0 }
console.log( iterator.next() );
>{ done: false, value:1 }
console.log( iterator.next() );
>{ done: false, value:2 }
console.log( iterator.next() );
>{ done: true } //4回目でdoneがtrueになる
上記の例では、getInteratorによってイテレータを作成しています。このとき、nextメソッドが返すオブジェクトのvalueは、レキシカルスコープに存在するvalue(※1)への参照を保持しています。このようにすることでクロージャの状態を作り出し、value++で1ずつインクリメントした値をレキシカルスコープの変数valueに保持しています。そのため、iterator.next()が呼び出されるために、1ずつインクリメントした値がvalueプロパティから取得できます。
文字列を返すイテレータ
次のイテレータは、アルファベットのa-zを順番に返します。
アルファベットをa-zを順番に返すイテレータ
function alphabetIterator( start = "a", end = "z"){
if ( start > end ){
throw "開始文字は終了文字より前のアルファベットを選んでください。";
}
//splitはStringオブジェクトのメソッドで引数で指定された文字で文字列を分割し、配列として返す
//なお、空文字("")が渡された場合には文字列を1文字ずつ分割する
const ALPHABET_ARRAY = "abcdefghijklmnoparstuvwxyz" . split(""); //アルファベットが1文字ずつ格納された配列を定義
//ALPHABET_ARRAY = [ 'a', 'b', 'c',…, "z" ];
const startIndex = ALPHABET_ARRAY.indexOf( start ); //startとendの文字を配列内で検索し、一致する要素のインデックスを取得
const endIndex = ALPHABET_ARRAY.indexOf( end);
const targetAlphabet = ALPHABET_ARRAY.slice( startIndex, endIndex + 1 ); //配列を指定の範囲で切り取り(endの文字も含めたいので+1する)
return {
next(){
const alphabet = targetAlphabet.shift(); //配列のshiftメソッドで配列の先頭から1ずつ要素を取り出す
//取り出す要素がなくなったとき、shift()はundefinedを返す
return {
value: alphabet, //取り出された要素をValueとして返す
done: alphabet ? false : true, //alphabetがundefinedのとき、trueを返す
}
}
}
}
const it = alphabetIterator( "c", "e" );
let nextValue = it.next();
while (){ //doneがtrueのとき、whileループを抜ける
console.log( nextValue );
nextValue = it.next(); //次の値をnextValueに格納
}
>{ done: false, value: "c" }
>{ done: false, value: "d" }
>{ done: false, value: "e" }
上記のコードでは、引数で渡されたアルファベットの範囲をALPHABET_ARRAYから切り出し、配列のshiftメソッドによって、切り出した配列(targetAlphabet)の先頭から1文字ずつアルファベットを返しています。また、while文を使うことで、doneがtrueになるまで、イテレータが実行されるように記述します。このようにイテレータは、開発者の実装次第で、数値だけではなく文字列の反復処理も表現できます。
イテレータの使いどころは、「イテレータは主にオブジェクトの反復処理の挙動を定義する」ために使われます。オブジェクトの特定のプロパティ(Symbol.iterator)にイテレータを返す関数を設定することによって、for・・・of文で反復できなかったオブジェクトを反復可能な状態にすることができます。
反復可能オブジェクト
反復可能オブジェクトとは、イテレータを保持しているオブジェクトのことです。MapやSet、Array、あるいはStringなどの一部のオブジェクトは、イテレータを内部に保持しています。そのため、for…of文などを使った反復処理に使用できます。
一方、オブジェクトリテラル{ }によって生成されるオブジェクトは、イテレータを内部に保持していません。そのため、オブジェクト{ }をfor…of文で使用できません。しかし、反復不可オブジェクトでも、イテレータを返す関数を特定のプロパティ(Symbol.iterator)に設定すれば、反復可能オブジェクトにすることが可能です。
オブジェクトにイテレータを設定する
反復可能オブジェクトを作成する場合は、イテレータを返す関数をオブジェクトのSymbol.iteratorプロパティに対して設定するという決まりがあります。このプロパティに対してイテレータを返す関数を設定すること、for…of文などの反復処理の制御を開発者が・追加・変更できます。
構文:イテレータを返す関数を使った反復可能オブジェクトの記法
オブジェクトに直接設定する場合
const iterableObject = {
[ Symbol.iterator ]: function() { return イテレータ }
};
//ES6のメソッドの省略法
const iterableObject = {
[ Symbol.iterator ]: () { return イテレータ }
}
コンストラクタ関数に設定する場合(Arrayの場合)
Array.prototype[ Symbol.iterator ] = function() { return イテレータ }
クラスに設定する場合
class IterableClass{
[ Symbol.iterator ]() { return イテレータ }
}
反復可能オブジェクトの作成
イテレータを返す関数をオブジェクトに設定して、反復可能オブジェクトを作成します。例として、for…of文による反復処理が可能で、ループするたびに0~2までの値を取得する反復可能オブジェクトを定義します。
0~2までの値を返す反復可能オブジェクト
const iterableObject = {
[ Symbol.iterator ](){
let value = 0;
return {
next(){
if ( value > 2 ){
return { done: true };
} else {
return {
done: false,
value: value++,
};
}
}
};
}
};
for ( const value of iterableObject ){
console.log( value );
}
オブジェクトにイテレータを追加した場合、オブジェクトをfor…of文のループに使用できます。また、このときループごとに取り出されれる値(Value)は、オブジェクトプロパティではなく、イテレータが返す値であることがわかります。このように、for…of文のを使った反復処理はただオブジェクトのプロパティを列挙しているのではなく、取得できる値はイテレータの挙動に準拠します。
コンストラクタにイテレータを設定
コンストラクタ関数のprototypeのSymbol.iteratorに対してイテレータを返す関数を設定します。
//イテレータの定義(渡された値を2倍する)
function doubleIterator() {
let index = 0;
let arry = this; //インスタンスにアクセスするときはthisを使用
return {
next() {
if ( index < arry.length ) { //インデックスが配列の長さより小さい場合には反復処理を継続
return {
done: false,
value: arry[ index++ ] * 2 //値を2倍にして返す
}
} else {
return {
done: true
}
}
}
}
}
for ( let item of [ 1,2,3 ] ){
console.log( item ); //配列の要素がそれぞれitemに渡される
}
>1
>2
>3
Array.prototype[ Symbol.iterator ] = doubleIterator; //既存のイテレータを上書き
for ( let item of [ 1,2,3 ] ){
console.log( item ); //2倍された値がitemに渡される
}
>2
>4
>5
上記例では、Array.prototypeのSymbol.iteratorプロパティに対して、新しく作成したイテレータを返す関数を登録しています。prototypeに設定されたプロパティは、プロトタイプチェーンによってインスタンス化したオブジェクトから呼び出すことができるため、Arrayオブジェクトのプロパティとして追加したときと同じような振る舞いをします。これによって、Arrayコンストラクタが保持している既存のイテレータの挙動は上書きされるため、for…of文の実行結果が変わります。