JY-CONTENTS

JY

JY-CONTENTS
search

+

MENU

【Javascript】イテレータの仕組みをしっかりと理解する

【Javascript】イテレータの仕組みをしっかりと理解する

(DATE)

-

2021.01.18

(CATEGORY)

-

イテレータとは

「イテレータ(反復子)」は繰り返しのための機構(プロトコル)です。
「イテレータ」という言葉に関連して以下の2つのオブジェクトがあります。
イテレータは以下の2つのオブジェクトである、という意味ではない。あくまで、言葉に関連するオブジェクト2つ。
イテレータを説明する上で必要な2つのオブジェクトと言う感じ。

  1. 反復子オブジェクト(イテレータオブジェクト iterator object)
  2. 反復可能なオブジェクト(iterable object)

この2つは相反するものではなく、「反復可能かつイテレータでもあるオブジェクト」もあります。そして、通常は「反復子オブジェクト(イテレータオブジェクト)」の意味で「イテレータ」という言葉を使います。

反復可能なオブジェクトは反復子オブジェクト(イテレータ)を所有しており、反復可能なオブジェクトに for…of 文を使う事で保有しているイテレータから値を取り出す(取得)、という感じになるます。

反復子オブジェクト(イテレータオブジェクト iterator object)

反復子オブジェクト(以下イテレータオブジェクト)は next メソッドを持っており、このメソッドを呼び出すことで、次々と要素を取り出せるオブジェクトのことを指します。

下記の(イテレータオブジェクトに変換した)配列の出力結果のように、イテレータオブジェクトは next を呼び出すと value と done という2つのプロパティをもつオブジェクトが返されます。
value は配列の値(イテレータから取り出した値)で、done はすべての要素を供給し終わったかどうかを示すフラグ(イテレータから値を順番に取り出し終えたかどうかの真偽値)になります。
配列の出力結果の最後はもう配列の値はないので done は false、value には「undefined」となっています。
イテレータオブジェクトは前の状態を記憶していて、next が呼ばれるたびに次のデータを1つづつ返します。

// 配列を反復子オブジェクト(イテレータオブジェクト iterator object)に変換
const num = [1, 2, 3, 4, 5].values();

console.log(num.next()) // { value: 1, done: false }
console.log(num.next()) // { value: 2, done: false }
console.log(num.next()) // { value: 3, done: false }
console.log(num.next()) // { value: 4, done: false }
console.log(num.next()) // { value: 5, done: false }
console.log(num.next()) // { value: undefined, done: true }

定義としては以下のような感じです。

var iterator = {}; // イテレータ
iterator.next = function(){ // nextメソッド
    var iteratorResult = { value: 42, done: false };
    return iteratorResult // valueとdone 2つのプロパティをもつオブジェクトを返す
};

反復可能なオブジェクト(iterable object)

反復可能なオブジェクトは、反復子オブジェクト(イテレータオブジェクト iterator object 以下イテレータオブジェクト)を持つオブジェクトです。
[Symbol.iterator] メソッドを実装する事でイテレータオブジェクト持ち、このメソッドを実行することで持っているイテレータオブジェクトを返します。
上記の事をオブジェクトに実装する事によって、そのオブジェクトを反復可能なオブジェクトに出来るという事になります。
配列、マップ、セットなどは反復可能なオブジェクトです。

[Symbol.iterator] メソッド
「イテレータプロトコル」を実装するには「Symbol.iterator」を実装する必要があります。Symbol.iterator はシンボルメソッド(シンボルをキーとしてもつプロパティであるメソッド)です。このメソッドはイテレータオブジェクトを返します。

以下は1~5までの値を取り出すイテレータオブジェクトをもった反復可能なオブジェクトです。

var obj = {}; // このオブジェクトを反復可能なオブジェクトにする
obj[Symbol.iterator] = function(){ // [Symbol.iterator]メソッドを実装
    var iterator = {}; // イテレータオブジェクトを持つ
    var count = 0;
    iterator.next = function () { // イテレータオブジェクトなのでnextメソッドを実装
        var iteratorResult = (count < 5)
            ? { value: ++count, done: false }
            : { value: undefined, done: true };
        return iteratorResult; // valueとdone 2つのプロパティをもつオブジェクトを返す
    };
    return iterator;  // イテレータオブジェクトを返す
};

console.log(obj[Symbol.iterator]()) // { next: [Function (anonymous)] } ← nextメソッドを持つオブジェクト
var it = obj[Symbol.iterator]() // 変数itにはiteratorオブジェクトが入る

console.log(it.next().value) // 1
console.log(it.next().value) // 2
console.log(it.next().value) // 3
console.log(it.next().value) // 4
console.log(it.next().value) // 5
console.log(it.next().value) // undefined

// ループで表示
let current = it.next();
while (!current.done) {
    console.log(current.value);
    // 1
    // 2
    // 3
    // 4
    // 5
    current = it.next()
}

ちょっとややこしいのでここでまとめてみると、

  1. 反復可能なオブジェクトを作成するには、そのオブジェクトはイテレータ(イテレータオブジェクト)を持つ必要がある。
  2. イテレータを持たせる為には [Symbol.iterator] メソッドを実装する必要がある。
  3. [Symbol.iterator] メソッドはイテレータを生成し、返すメソッド。
    [Symbol.iterator] メソッド内ではイテレータを生成し、返すコードを記述する。
  4. まとめのまとめ
    あるオブジェクトに[Symbol.iterator] メソッドを実装し、[Symbol.iterator] メソッド内でイテレータを生成、実装し、返すコードを記述する事で反復可能なオブジェクトを作成する事ができる。

for…of 文を使う

反復可能なオブジェクトは、 for…of を使って保有しいるイテレータの値を取り出す事ができます。
いままで見てきたサンプルコードよりもシンプルに取り出す事ができます。

var obj = {};
obj[Symbol.iterator] = function () {
    var iterator = {};
    iterator.next = function () {
        var iteratorResult = (count < 5)
            ? { value: ++count, done: false }
            : { value: undefined, done: true };
        return iteratorResult;
    };
    return iterator;
};

// for...ofでイテレータの値を取り出す
for(var v of obj) console.log(v);
// 1
// 2
// 3
// 4
// 5

【処理の流れ】

  1. まずobj[Symbol.iterator]()を実行して、オブジェクトiteratorを取得
  2. 次にiterator.next()を実行して、iteratorResultを取得
  3. iteratorResult.done の値を確認し「true」なら処理を終了。「false」なら 4. に進む
  4. iteratorResult.valueの値を取得
  5. for…ofの変数vに4.で取得した値が入り、console.log(v)で表示。
  6. 2. に戻る

配列について

配列は反復可能なオブジェクト(iterableobject)です。
先の説明からも、配列は反復可能なオブジェクトなので、イテレータ(反復子オブジェクト)を持つオブジェクトということになります。
配列以外にも文字列、マップ、セットなども反復可能なオブジェクトになります。

以下のコードから、配列は [Symbol.iterator] メソッドを実装しており、そのメソッドを実行することでイテレータを返しているので、配列はイテレータ(反復子オブジェクト)を持つオブジェクトである事がわかります。

const num = [1, 2, 3]
// [Symbol.iterator]メソッドを実行
// 変数itにはイテレータオブジェクトが入る
var it = num[Symbol.iterator]()
// 変数itはイテレータオブジェクトなのでnextメソッドで値を取得できる
console.log(it.next()) // { value: 1, done: false } 
console.log(it.next()) // { value: 2, done: false }
console.log(it.next()) // { value: 3, done: false }
console.log(it.next()) // { value: undefined, done: true }

for…of 文

もちろん for…of 文でも値を取り出す事ができます。

const num = [1, 2, 3]
for(var v of num) console.log(v);
// 1
// 2
// 3

values メソッド

配列は反復可能なオブジェクトであって、イテレータ自身ではありません。
イテレータとして使うには [Symbol.iterator] メソッドを実行してイテレータを取得する必要がります。
以下のようにするともちろんエラーになります。

const num = [1, 2, 3] // 配列はイテレータオブジェクトではない
// nextメソッドを使って要素を取り出そうとするとエラーになる
console.log(num.next()) // TypeError: num.next is not a function

そこで簡単に配列をイテレータオブジェクト に変換するのに values メソッドを使います。

const num = [1, 2, 3].values(); // 配列をイテレータに変換

console.log(num.next()) // { value: 1, done: false }
console.log(num.next()) // { value: 2, done: false }
console.log(num.next()) // { value: 3, done: false }
console.log(num.next()) // { value: undefined, done: true }

values メソッドは以下と同等の働きかと思います。
配列は反復可能なオブジェクトなので[Symbol.iterator] メソッドを持っている。

const num = [1, 2, 3][Symbol.iterator]() // values メソッドと同等  

console.log(num.next()) // { value: 1, done: false } 
console.log(num.next()) // { value: 2, done: false } 
console.log(num.next()) // { value: 3, done: false } 
console.log(num.next()) // { value: undefined, done: true }

クラスのインスタンスを反復可能なオブジェクトに

以下は配列を保有しているシンプルなクラスです。
配列の値を1つずつ取得するには以下のようになります。

class Sam {
    constructor() {
        this.nums = [1, 2, 3];
    }
}

let a = new Sam()
console.log(a.nums[0]) // 1
console.log(a.nums[1]) // 2
console.log(a.nums[2]) // 3

インスタンスを反復可能なオブジェクトにする事でfor…of 文を使いよりシンプルに配列の値を取得する事ができます。
[Symbol.iterator] メソッドを実装する事でインスタンスを反復可能なオブジェクトにします。

class Sam {
    constructor() {
        this.nums = [1, 2, 3];
    }

    // シンボルメソッド(イテレータオブジェクトを返すメソッド)を実装
    [Symbol.iterator]() {
        // 配列をvaluesメソッドでイテレータオブジェクトに変換
        return this.nums.values();
        // もしくは
        // return this.nums[Symbol.iterator]();
    }
}

let a = new Sam()
for(var v of a) console.log(v);
// 1
// 2
// 3

[Symbol.iterator] メソッドはイテレータを返すので、next メソッドでも値を取得できます。

class Sam {
    constructor() {
        this.nums = [1, 2, 3];
    }

    [Symbol.iterator]() {
        return this.nums.values();
    }
}

let a = new Sam()
// [Symbol.iterator]メソッド実行でイテレータを取得
let it = a[Symbol.iterator]()
console.log(it.next()) // { value: 1, done: false }
console.log(it.next()) // { value: 2, done: false }
console.log(it.next()) // { value: 3, done: false }
console.log(it.next()) // { value: undefined, done: true }

ここまでは、values メソッドを使って、配列をイテレータオブジェクトに変換してイテレータを実現していましたが、values メソッドを使わずに独自のイテレータを実装する事もできます。

class Sam {
    constructor() {
        this.nums = [1, 2, 3];
    }

    // valuesメソッドを使わずにイテレータオブジェクトを返す
    [Symbol.iterator]() {
        let i = 0;
        const nums = this.nums;
        // nextメソッドを実装したオブジェクト
        return {
            // nextはvalueとdoneをもつオブジェクトを返すメソッド
            next: () => i >= nums.length
                ? { value: undefined, done: true }
                : { value: nums[i++], done: false }
        }
    }
}

let a = new Sam()
for (var v of a) console.log(v);
// 1
// 2
// 3

クロージャ

以下コードはクロージャ(関数)を2つの関数内で実行しています。
クロージャはローカル変数の参照を維持しますので、以下のようになってしまいます。

const getNumber = (function () {
    let num = 0;
    return function () {
        return ++num;
    };
})();

function sam1() {
    console.log(`sam1: ${getNumber()}`)
    console.log(`sam1: ${getNumber()}`)
}

function sam2() {
    console.log(`sam2: ${getNumber()}`)
    console.log(`sam2: ${getNumber()}`)
}

// sam1、sam2別々で加算ができない
sam1()
sam2()

// 出力
// sam1: 1
// sam1: 2
// sam2: 3
// sam2: 4

こうした場合はクロージャではなくイテレータが役立ちます。

function getNumber() {
    let num = 0;
    return { // イテレータオブジェクトを返す
        next() {
            return { value: ++num, done: false };
        }
    };
}

function sam1() {
    const a = getNumber();
    console.log(`sam1: ${a.next().value}`)
    console.log(`sam1: ${a.next().value}`)
}

function sam2() {
    const a = getNumber();
    console.log(`sam2: ${a.next().value}`)
    console.log(`sam2: ${a.next().value}`)
}

// sam、sam2別々で加算ができる
sam1()
sam2()

// 出力
// sam1: 1
// sam1: 2
// sam2: 1
// sam2: 2

イテレータオブジェクトを返す事で、呼び出しの next メソッドはオブジェクトという文脈コンテキストの中で動作しているので、この振る舞いはそのオブジェクトによって制御されています。getNumber を異なる場所(別の関数)で利用すれば、別のイテレータを生成し、ほかのイテレータとの干渉は起こらなくなります。

まとめ

  • イテレータであるためには next メソッドを実装している必要がある。
  • 反復可能(iterable)であるためには [Symbol.iterator] メソッドを実装している必要がある。[Symbol.iterator] メソッドはイテレータを返すので、反復可能なオブジェクトからイテレータは簡単に作ることができる。
  • 配列、文字列、マップなどは反復可能なオブジェクトなので、for…ofを使って順番に要素を処理できる。

NEW TOPICS

/ ニュー & アップデート

SEE ALSO

/ 似た記事を見る

JY CONTENTS UNIQUE BLOG

search-menu search-menu