0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaScriptのIteratorについて

Posted at

初めに

今回はイテレータの基本概念、[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
    (ここではプリミティブタイプ/オブジェクトタイプで分けたのではなくデータ構造として分けられている。)

オーダー持てない普通のオブジェクトなら整列されてないため、どこから始まる?どこまで終わる?っていう計算できない状態になるのでイテレーションの特徴に合わないのです。MapSetはインデックスで要素を呼び出すことができないけれど、計算できるプロパティが付与するために要素を[]で包んで格納し、整列できる構造で構築していく。Stringはコンパイルでは文字コードのように参照できたり各自に長さを持っていたりしている。そしてargumentsNodeListは構造上プロパティを列挙可能なオーダー(数値)に組み込まれている。これらがすべてイテレーションの特徴によって創られました。

イテレータ(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' ]

0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?