現代的JavaScriptおれおれアドベントカレンダー2017 – 23日目
概要
function*
で、イテレータを返すジェネレータ関数を作成できます。中で yield
が使えます。
function* createIdGenerator() { let currentId = 100; while (true) { yield currentId++; } } const iterator = createIdGenerator(); console.log(iterator.next().value); console.log(iterator.next().value); console.log(iterator.next().value);
これはオブジェクトを反復可能にするのに便利です。
const obj = { *[Symbol.iterator]() { const max = 3; for (let i = 0; i < max; i++) { yield i; } } }; for (let item of obj) { console.log(item); }
イテレータ
著名なデザインパターンのひとつです。配列でないオブジェクトでも、共通のインターフェイスで反復処理を行えるようにします。
JavaScript (ES2015+) では(たぶん一般的な定義とは異なり)イテレータは next()
メソッドを持つオブジェクトです。具体的なインターフェイスは後述します。
JavaScriptでのイテレータ
イテラブルなオブジェクトで [Symbol.iterator]()
というすごい名前のメソッドを実行すると、イテレータを得られます。
const iterable = new Set([100, 200, 300]); // イテレータ生成 const it = iterable[Symbol.iterator](); while (true) { // 反復処理を進め、現段階の状態を得る const result = it.next(); // 繰り返し条件を確認 if (result.done) { break; } // 項目取得 const item = result.value; console.log(item); }
Set
オブジェクトはイテラブルなオブジェクトなので、 [Symbol.iterator]()
を実行するとイテレータ it
を生成して返してくれます。
そのイテレータ it
は next()
メソッドを持ちます。このメソッド呼び出しを繰り返して反復 (iterate) していきます。
next()
メソッドは実行するたびに、なんだろ、結果オブジェクト? result
を生成して返します。結果オブジェクトって呼び方で良いかな。 IteratorResult
という名前のインターフェイス(後述)を満たすオブジェクトです。
この結果オブジェクトはまた、二つのプロパティを持ちます。 value
が反復途中の現段階の値で、 done
がもう反復し終えたかが格納される真偽値です。
これらを使って配列でも何でもぐるぐるできます。
for-of
で使う
オブジェクトがイテラブルである、つまり前述の各要素を満たす場合、 for-of
を使って簡単に反復処理を実現できます。
const iterable = new Set([100, 200, 300]); for (let item of iterable) { console.log(item); }
わあ短い。
ジェネレータで生成する
[Symbol.iterator]()
をちゃんと自作すればどんなオブジェクトでもイテラブルにできます。
next()
メソッドを自前で実装しても良いんだけど、面倒なので、ジェネレータ関数というのを使って作ると簡単です。
ジェネレータ
function*
を使ってジェネレータ関数を宣言できます。ジェネレータ関数を実行するとジェネレータオブジェクトを得られます。
function* createGenerator() { const values = [100, 200, 300]; for (let i = 0; i < values.length; i++) { const item = values[i]; yield item; } } const generator = createGenerator();
ジェネレータオブジェクトは加えてイテラブルでもあるで for-of
で使えます。わーい。
for (let item of generator) { console.log(item); }
generator[Symbol.iterator]()
の結果は元のオブジェクト generator
になります。
また同時にイテレータでもあるので next()
を持ちます。コードは書かなくても良いかね。
yield
さらっと使ったけど、ジェネレータ関数の中では yield
という式を使えます。この式は右辺を value
、また done: false
という設定で、 next()
で返す IteratorResult
オブジェクトを生成します。
必ずしも for
文で使う必要はないです。
function* oneTwoThree() { yield 1; yield 2; yield 3; } const it = oneTwoThree(); let result; result = it.next(); console.log(result.value); // => 1 result = it.next(); console.log(result.value); // => 2 result = it.next(); console.log(result.value); // => 3
[Symbol.iterator]()
を実装する
いよいよ [Symbol.iterator]()
です。 *
を付けて、ジェネレータ関数というかジェネレータメソッドになります。
const obj = { *[Symbol.iterator]() { const max = 3; for (let i = 0; i < max; i++) { yield i; } } }; for (let item of obj) { console.log(item); }
他のイテレータ生成メソッドを作る
obj
じゃなくて obj.iterator()
のようにメソッドを呼んでやる必要があるけれど、他の名前でも大丈夫。
目的別に何通りか用意しても良いかもね。
const obj = { *iterator() { const max = 3; for (let i = 0; i < max; i++) { yield i; } } }; for (let item of obj.iterator()) { console.log(item); }
イテレータのインターフェイス
いっぱい出てきたのでまとめ。
- 反復可能
( なオブジェクト …[Symbol.iterator]()
をもつオブジェクト(Iterable
) [Symbol.iterator]()
… イテレータを返すメソッド- イテレータ …
next()
をもつオブジェクト(Iterator
) next()
…value
、done
を持つオブジェクト(IteratorResult
)を返し、内部状態を進めるメソッドvalue
… 反復処理各段階における値done
… 反復処理が終了しているかどうか
[Symbol.iterator]()
Symbol.iterator
というシンボルが、名前というか何だろ、識別子?になってるメソッドです。 []
は動的にプロパティ名を決めるやつです。別稿参照。あとシンボル Symbol
についても。
なんならどこかで Symbol.iterator = 'iterate'
とか定義されてると考えてください。
Iterable
インターフェイス
オブジェクトがこのインターフェイスを満たしていると、 for-of
とか ...
とかが使えます。
[Symbol.iterator]()
… イテレータオブジェクトを返す
Iterator
インターフェイス
このインターフェイスを満たすオブジェクトをイテレータオブジェクトと呼びます。
next()
… イテレータオブジェクトを返すreturn()
(optional) … イテレータオブジェクトを返すthrow()
(optional) … イテレータオブジェクトを返す
return()
を呼び出すと「もう終了するぞ」と、 throw()
を呼び出すと「なんかおかしいぞ」を、呼び出し先となるオブジェクトに伝えることが、できる、そう、です。単純に配列みたいな情報を扱う上ではいらなさそうだけど、何か外部から情報を引っ張ってくるようなやつで使うときに便利なんだろか。わかんない。
IteratorResult
インターフェイス
next()
他のメソッドはこのインターフェイスを満たすオブジェクトを返す必要があります。
done
value
分割代入やスプレッド演算子 ...
これら実はイテラブルオブジェクトが対象です。配列じゃなくても自作のオブジェクトでも、 [Symbol.iterator]()
を備えていれば使えます。
function* oneTwoThree() { yield 1; yield 2; yield 3; } // 分割代入 const [one, two, three] = oneTwoThree(); console.log(one); console.log(two); console.log(three); // スプレッド演算子 function say(one, two, three) { console.log(one); console.log(two); console.log(three); } say(...oneTwoThree());
ES2018では普通のオブジェクトも分割代入したりスプレッド演算子で展開したりできるようになるっぽいんだけど、じゃあ普通のオブジェクトも全部イテラブルになるのかな? 仕様策定の様子は追ってないので思っただけだけど。
分割代入と ...
の使い方は別稿参照。
- 分割代入、配列も画期的。(現代的JavaScriptおれおれアドベントカレンダー2017 – 15日目)
- 配列を「開いて」使うスプレッド演算子。(現代的JavaScriptおれおれアドベントカレンダー2017 – 18日目)
その他
[Symbol.iterator]()
て何だよ
なんで普通の名前じゃないんだ Symbol
なんだ、 toString()
みたいにしなかったんだ。
だいたいイテレータはイテラブル
「イテレータ」と「イテラブル」は別インターフェイスなので同時に満たす必要はないんだけど、内部で %IteratorPrototype%
というイテレータ共通のプロトタイプがありまして、こいつがイテラブルだったりします。
配列とかのイテレータはこのプロトタイプを継承しているので、JavaScriptネイティブなイテレータはだいたいイテラブルになるっぽい。 TypedArray
は専用のものを持たないが、普通の配列 Array
の処理を利用している。
ちなみに %IteratorPrototype%
の [Symbol.iterator]()
は this
を返します。
イテラブルではないイテレータ
の例。
class MyIterator { constructor(values) { this.values = values; this.index = 0; } // 自前で実装するぞ next() { const value = this.values[this.index]; this.index += 1; return { value: value, done: this.index >= this.values.length, }; } }; const values = [100, 200, 300]; // イテレータとして利用 const it = new MyIterator(values); let result; result = it.next(); console.log(result.value); // イテラブルとして利用(できない) // Exception: TypeError: (new MyIterator(...)) is not iterable for (let item of new MyIterator(values)) { console.log(item); }
普通のイテレータ
Java方面の話で聞いたときは next()
で次に移動しつつ値を戻り値でそのまま得て、続きがあるかどうかは hasNext()
という別のメソッドを使うみたいな流れだったと思うんだけど、なんで違うのかしらん。
時代が変わってイテレータパターン自体が変わった?
インターフェイス vs プロトコル
ちなみにMDNだとイテレータとかで「プロトコル (protocol)」という表現が用いられているけれど、仕様書だと「インターフェイス (interface)」です。まあ同じものでしょ。
たしかSwiftはprotocolという表現してたよね。
function
と *
の間で改行できる
適当に空白文字を置ける様子。
function * goo() { }
async
はだめだったのに~。
参考
- ECMAScript® 2017 Language Specification
- 7.4 Operations on Iterator Objects
- 14.4 Generator Function Definitions
- 21.1.5 String Iterator Objects
- 22.1.5 Array Iterator Objects
- 22.1.5.2 The %ArrayIteratorPrototype% Object …
%IteratorPrototype%
を継承 - 22.2 TypedArray Objects
- 22.2.3.31 %TypedArray%.prototype [ @@iterator ] ( ) … 結局普通の配列と同じようなイテレータを生成
- 23.1.5 Map Iterator Objects
- 23.2.5 Set Iterator Objects
- 25.1 Iteration … 各種インターフェイス
- 25.1.2 The %IteratorPrototype% Object …
[Symbol.iterator]()
を持つプロトタイプ - 25.3 Generator Objects
- 25.3.1 Properties of Generator Prototype …
%IteratorPrototype%
を継承 - 14.4.13 Runtime Semantics: Evaluation … ジェネレータ関数の解析。
yiled
からIteratorResult
オブジェクトを生成したり
- function* – JavaScript | MDN
- イテレーターとジェネレーター – JavaScript | MDN
- Generator – JavaScript | MDN
- Iterator パターン – Wikipedia
- デザインパターンの使い方:Iterator (1/2):CodeZine(コードジン) … 1995年発行の書籍で初めてイテレータパターンが紹介された
更新履歴
- 2017/12/23
TypedArray
がイテラブルじゃないみたいな勘違いしてたのを修正