初めに
今回はイテレータの基本概念、[Symbol.iterator]
組み込みの仕方、反復構文/メソッドについてまとめてみました。
Memo
すでにイテレータ@@iterator
が組み込んでいる(Built‑in natives)反復可能オブジェクト:
- Array
- Map
- Set
- String
- TypedArray
- arguments object(in function)
- NodeList object
反復可能オブジェクトを受け入れる一部のAPI
:
- new Map([iterable])
- new Set([iterable])
- new WeakMap([iterable])
- new WeakSet([iterable])
- Promise.all([iterable])
- Promise.race([iterable])
- Array.from([iterable])
Iteratable
これらのデータ構造がイテレータできるのはもともと
[Symbol.iterator]
プロパティが組み込まれているからです。つまりほかのデータ構造(普通のオブジェクトなど)に[Symbol.iterator]
メソッドを組み込めばイテレータできるようになります。
(データ構造のまとめがこれからメモの読みやすさのために一番上のMemo
に移した。)これは参考文章から取り出した一部の説明をまとめた日本語訳です。しかし[Symbol.iterator]
を理解するためにもっと基本的な概念が必要だと、これらのデータ構造の特徴と共通点も少しまとめたいと思います。
イテレーションというのは、順序づけられた(ordered)の要素へ一連の結果(sequence of outcomes)を返すという繰り返す行為(repetition)です。つまり、
- 順序づけ
-
一連に繰り返せる構造
が最大の特徴だと思います。
そしてMemo
に書いたデータ構造たちは、配列か、配列のようにオーダー(インデックスなど)を持っている構造だからこそイテレータできるのです。これに沿って大きく分けてみると、
- 配列構造:Array, TypedArray
-
オーダーを持つオブジェクト:Map, Set, String, arguments object, NodeList object
(ここではプリミティブタイプ/オブジェクトタイプで分けたのではなくデータ構造として分けられている。)
オーダー持てない普通のオブジェクトなら整列されてないため、どこから始まる?どこまで終わる?っていう計算できない状態になるのでイテレーションの特徴に合わないのです。Map
とSet
はインデックスで要素を呼び出すことができないけれど、計算できるプロパティが付与するために要素を[]
で包んで格納し、整列できる構造で構築していく。String
はコンパイルでは文字コードのように参照できたり各自に長さを持っていたりしている。そしてarguments
やNodeList
は構造上プロパティを列挙可能なオーダー(数値)に組み込まれている。これらがすべてイテレーションの特徴によって創られました。
イテレータ(iterator)はこの概念に基づいて一連の結果を生成するメソッドでありプロパティでもあります。イテレータはデータ構造のポインタ(pointer)のような役割を担って、next()
メソッドを通してポインタを転がし計算結果を返してくるのです。
// iterable
// function makeIterator(arr) {
// let nextIndex = 0;
// return {
// next() {
// return nextIndex < arr.length ?
// { value: arr[nextIndex++], done: false } :
// { value: undefined, done: true };
// }
// };
// }
function makeIterator(arr) {
let nextIndex = 0;
return {
next() {
return nextIndex < arr.length ?
{ value: arr[nextIndex++] } :
{ done: true };
}
}
}
let test = makeIterator(['a', 'b']);
console.log(test.next()); // { value: 'a', done: false }
console.log(test.next()); // { value: 'b', done: false }
console.log(test.next()); // { value: undefined, done: true }
イテレータは一種のインターフェース(接点)、つまりデータ構造自体ではなく仲立ち、あるいはデータ構造のシミュレーションするメソッドです。
例えば下のように無限にデータを生成するイテレータメソッドidMaker()
では特定のデータ構造へのイテレーションではなく、データ構造を生成するシミュレーションです。
function idMaker() {
let index = 0;
return {
next() {
return { value: index++, done: false };
}
};
}
let test = idMaker()
console.log(test.next()); // { value: 0, done: false }
console.log(test.next()); // { value: 1, done: false }
console.log(test.next()); // { value: 2, done: false }
シミュレーションができるというのは、イテレーションの特徴がなくてもイテレータメソッドだけでイテレータできる(iterable)データ構造を作り出すことができる。
Iterator - [Symbol.iterator]
[Symbol.iterator]
はJavaScriptの組み込みイテレータメソッド(プロパティ)です。下のようにすでに組み込んでいるデータ構造なら[Symbol.iterator]()
で呼び出すことができます。
let arr = ['a', 'b', 'c'];
let arrIterator = arr[Symbol.iterator]();
console.log(arrIterator.next()); // { value: 'a', done: false }
console.log(arrIterator.next()); // { value: 'b', done: false }
let str = '123';
let strIterator = str[Symbol.iterator]();
console.log(strIterator.next()); // { value: '1', done: false }
console.log(strIterator.next()); // { value: '2', done: false }
無秩序なオブジェクトでもこのプロパティを組み込めばイテレータできるようになります。
const obj = {
[Symbol.iterator]: function () {
let index = 0;
return {
next: function () {
return {
value: index++
};
}
};
}
};
let objIterator = obj[Symbol.iterator]()
console.log(objIterator.next()); // { value: 0 }
console.log(objIterator.next()); // { value: 1 }
class
も、[Symbol.iterator]() { return this; }
ではインスタンス自分のオブジェクトもこのプロパティを持ち、そしてイテレーションはRangeIterator.prototype
からnext()
メソッドを呼び出せばイテレータできます。
// class
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() {
return this;
}
next() {
let value = this.value;
if (value < this.stop) {
this.value++;
return { done: false, value: value };
}
return { done: true, value: undefined };
}
}
console.log(Object.getOwnPropertyNames(RangeIterator.prototype));
// [ 'constructor', 'next' ]
function range(start, stop) {
return new RangeIterator(start, stop);
}
for (let value of range(0, 3)) {
console.log(value);
}
// 0
// 1
// 2
カスタマイズの配列風オブジェクトにはオーダーのようなプロパティを設置してから配列の[Symbol.iterator]
プロパティを借りればイテレータできます。
// arguments
function printArgs() {
console.log(arguments);
for (let item of arguments) {
console.log(item);
}
}
printArgs('a', 'b');
// [Arguments] { '0': 'a', '1': 'b' }
// a
// b
//
// array-like
let notNodeList = {
0: 'div.container',
1: 'div.card__container',
2: 'div.card__img',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
console.log(notNodeList[Symbol.iterator]());
// Object [Array Iterator] {}
console.log(notNodeList[Symbol.iterator]().next());
// { value: 'div.container', done: false }
console.log([...notNodeList]);
// [ 'div.container', 'div.card__container', 'div.card__img' ]
for (let element of notNodeList) {
console.log(element);
}
// div.container
// div.card__container
// div.card__img
//
let arr = ['div.container', 'div.card__container', 'div.card__img'];
let arrayLikeObj = {
...arr
};
console.log(arrayLikeObj);
// {
// '0': 'div.container',
// '1': 'div.card__container',
// '2': 'div.card__img'
// }
配列風のようにオーダーづけられてるのでなければ、配列の[Symbol.iterator]
を使っても対応するプロパティが見つからずundefinec
になります。
別のやり方としては[Symbol.iterator]
プロパティをジェネレータと組み合わせてyield
で結果を返してもらうことができます。
// unordered object
let obj = {
a: 'a',
b: 'b',
c: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of obj) {
console.log(item);
}
// undefined
// undefined
// undefined
//
let obj = {
from: 0,
to: 3,
*[Symbol.iterator]() {
for (let i = this.from; i < this.to; i++) {
yield String.fromCodePoint(97 + i)
}
}
};
for (let item of obj[Symbol.iterator]()) {
console.log(item);
}
// a
// b
// c
ジェネレータについて前の文章では少しまとめてみました。
Syntax
配列が引数として受け入れられたり、あるいは返り値になったりするメソッドを使用すると暗黙に[Symbol.iterator]
プロパティを呼び出します。
Destructuring assignment & Spread syntax
// Destructuring assignment & Spread syntax
let set = new Set(['a', 'b', 'c']);
for (let item of set) {
console.log(item);
};
// a
// b
// c
//
let arr = [...set]; // Spread syntax
console.log(arr); // [ 'a', 'b', 'c' ]
let [first, ...rest] = set; // Destructuring assignment
console.log(first, rest); // a [ 'b', 'c' ]
//
let str = 'Apple';
console.log(...str); // A p p l e
console.log([...str]); // [ 'A', 'p', 'p', 'l', 'e' ]
//
let arr = ['Banana', 'Lemon']
console.log(['a', ...arr, 'Peach']); // [ 'a', 'Banana', 'Lemon', 'Peach' ]
Generator & yield*
// Generator & yield*
function* generator() {
yield 1;
yield* [2, 3, 4];
yield 5
}
let iterator = generator();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
//
let callGenerator = function* () {
yield* generator()
};
let test = callGenerator()
console.log(test.next()); // { value: 1, done: false }
console.log(test.next()); // { value: 2, done: false }
console.log(test.next()); // { value: 3, done: false }
//
for (let item of test) {
console.log(item);
}
// 1
// 2
// 3
// 4
// 5
// Practices
let obj = {
data: ['Apple', 'Banana', 'Lemon', 'Peach'],
*[Symbol.iterator]() {
for (let i = 0; i < this.data.length; i++) {
yield [i, this.data[i]]
}
}
};
for (let [index, value] of obj) {
if (index > 2) break
console.log(value);
}
// Apple
// Banana
// Lemon
//
let obj = {
a: 'Apple',
b: 'Banana',
c: 'Lemon',
d: 'Peach',
*[Symbol.iterator]() {
for (let i = 0; i < 26; i++) {
yield [i, this[String.fromCodePoint(97 + i)]]
}
}
};
for (let [index, value] of obj) {
if (index > 2) break
console.log(value);
}
// Apple
// Banana
// Lemon
for...of
[Symbol.iterator]
プロパティを持てばfor...of
を使用するとイテレータを呼び出すことができます。
// for...of
// Array
let arr = ['Apple', 'Banana', 'Lemon'];
for (let item of arr) {
console.log(item);
}
// Apple
// Banana
// Lemon
//
let obj = {};
obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);
// obj[Symbol.iterator] = Array.prototype[Symbol.iterator].bind(arr);
for (let item of obj) {
console.log(item);
}
// Apple
// Banana
// Lemon
// extract index without using for...in
console.log(Object.entries(obj)); // []
// note: obj is still empty, we just bought iterator method and data from arr
for (let [index, value] of [...obj].entries()) {
console.log(`index: ${index}, value: ${value}`);
}
// index: 0, value: Apple
// index: 1, value: Banana
// index: 2, value: Lemon
// if property name wasn't number in order, property will be skipped
let arrayLikeObj = {
2: 'a',
0: 'b',
1: 'c',
foo: 'foo',
a: 'a',
length: 5,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of arrayLikeObj) {
console.log(item);
}
// b
// c
// a
// undefined
// undefined
//
let obj = {
2: 'a',
0: 'b',
1: 'c',
foo: 'foo',
a: 'a',
length: 5,
}
for (let item of Object.keys(obj)) {
console.log(item);
}
// 0
// 1
// 2
// foo
// a
// length
for (let item of Object.values(obj)) {
console.log(item);
}
// b
// c
// a
// foo
// a
// 5
Array.from()
// Array.from()
let arrayLikeObj = {
0: 'a',
1: 'b',
2: 'c',
length: 3 // required
};
let arr = Array.from(arrayLikeObj);
console.log(arr); // [ 'a', 'b', 'c' ]
Map()
& Set()
& WeakMap()
& WeakSet()
// Map()
let map = new Map([
[0, 'a'],
[1, 'b'],
[2, 'c']
]);
console.log(...map); // [ 0, 'a' ] [ 1, 'b' ] [ 2, 'c' ]
for (let [key, value] of map.entries()) {
console.log(`key: ${key}, value: ${value}`);
}
// key: 0, value: a
// key: 1, value: b
// key: 2, value: c
console.log(map[Symbol.iterator] === map.entries); // true
Map()
、Set()
、WeakMap()
、WeakSet()
と静的メソッドの勉強メモは前は書きました。
Promise.all()
& Promise.race()
PromiseAPI
には一部配列しか受け入れられないメソッドもイテレータ[Symbol.iterator]
を呼び出します。
// Promise.all() & Promise.race()
let promise1 = Promise.resolve(3);
let promise2 = 42;
let promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// [ 3, 42, 'foo' ]