JSにおけるデータ構造のエルゴード--Iteratorとfor...ofサイクル

9938 ワード

JavaScriptは元の4種類の「集合」を表すデータ構造で、Object、Aray、Set、Map.
エルゴード(Iterator)は、様々な異なるデータ構造のための統一的なアクセス機構を提供するインターフェースである.任意のデータ構造は、Iteratorインターフェースを配備すれば、エルゴード動作を完了することができます.
Iteratorの役割は三つあります.一つは各種データ構造のために、統一的で簡便なアクセスインターフェースを提供します.第二は、データ構造のメンバーをある順序で並べられるようにすることである.第三に、ES 6は新しいエルゴードコマンドfor...ofループを作成しました.Iteratorインターフェースは主にfor...of消費に提供されます.
Iteratorの遍歴過程はこうです.
  • は、現在のデータ構造の開始位置を指すポインタオブジェクトを作成します.エルゴードオブジェクトは、本質的にはポインタオブジェクトです.
  • ポインタオブジェクトのnextメソッドを初めて呼び出し、ポインタをデータ構造の最初のメンバに向けることができる.
  • ポインタオブジェクトのnextメソッドを第2回呼び出すと、ポインタはデータ構造の第2のメンバを指す.
  • は、ポインタオブジェクトのnextメソッドを、データ構造の終了位置を指すまで継続的に起動する.
  • nextメソッドを呼び出すたびに、データ構造の現在のメンバーの情報を返します.具体的には、valueとdoneの2つの属性を含むオブジェクトを返します.ただし、value属性は現在のメンバの値であり、done属性はブール値であり、遍歴が終了するかどうかを示す.
    nextメソッドをシミュレートして値を返した例
    let it = makeIterator(['a','b']);
    
    it.next();// { value: "a", done: false }
    it.next();// { value: "b", done: false }
    it.next();// { value: undefined, done: true }
    
    fucntion makeIterator(array){
        let nextIndex = 0;
        return {
            next : function(){
                return nextInedx < array.length ? 
                {value : array[nextIndex++], done : false} : 
                {value : undefined, done : true};
            }
        }
    }
    
    デフォルトIteratorインターフェース
    Iteratorインターフェースの目的は、すべてのデータ構造のための統一的なアクセス機構、すなわちfor...ofサイクルを提供することである.for...ofを使用してあるデータ構造を巡回すると、このサイクルは自動的にIteratorインターフェースを探しに行く.
    ES 6は、デフォルトのIteratorインターフェースがデータ構造のSymbol.iterator属性に配置されていると規定しています.あるいは、一つのデータ構造はSymbol.iterator属性を持つ限り、「巡回可能」と考えられます.
    Symbol.iterator属性自体は関数であり、現在のデータ構造のデフォルトのエルゴード生成関数である.この関数を実行すると、エルゴードが返されます.属性名Symbol.iteratorについては、Symbolオブジェクトのiterator属性を返します.これはあらかじめ定義されていて、タイプはSymbolの特殊な値ですので、括弧内に入れます.
    const obj = {
        [Symbol.iterator] : function(){
            return {
                next : function(){
                  return {
                      value : 1,
                      done : true
                  }  
                }
            }      
        }
    };
    
    上のコードでは、オブジェクトobjはSymbol.iterator属性を持つので、巡回可能です.この属性を実行すると、エルゴードオブジェクトが返されます.このオブジェクトの基本的な特徴は、nextメソッドを持つことです.nextメソッドを呼び出すたびに、現在のメンバーを表す情報オブジェクトが返されます.valueとdoneの2つの属性があります.
    ES 6のいくつかのデータ構造は、元々は、配列のようなIteratorインターフェースを備えています.つまり、何も処理しなくても、for...ofループによって巡回されます.これらのデータ構造はSymbol.iterator属性を展開しています.他のいくつかのデータ構造はありません.Symbol.iterator属性が展開されているデータ構造は、エルゴードインターフェースと呼ばれる.このインターフェースを起動すると、エルゴードオブジェクトが返されます.
  • Aray
  • Map
  • セット
  • String
  • TypedAray
  • 関数のargmentsオブジェクト
  • NodeListオブジェクト
  • これらのデータ構造は、元のインターフェースを備えています.
    配列のSymbol.iterator属性.
    let arr = ['a','b','c'];
    
    let iter = arr[Symbol.iterator]();
    
    iter.next() // { value: 'a', done: false }
    iter.next() // { value: 'b', done: false }
    iter.next() // { value: 'c', done: false }
    iter.next() // { value: undefined, done: true }
    
    上のコードでは、変数arrは配列であり、元はエルゴードインターフェースを持ち、arrのSymbol.iterator属性の上に配置されています.したがって、この属性を呼び出すと、エルゴードオブジェクトが得られます.
    Iteratorインターフェースを原生で展開するデータ構造については、自分でエルゴードを書かずに関数を生成します.for...ofサイクルは自動的にそれらを巡回します.その他のデータ構造(主に対象)のIteratorインターフェースは、Symbol.iterator属性の上に自分で配置する必要があります.for...ofによってIteratorインターフェースを循環的に起動するには、Symbol.iteratorの属性に属性エルゴード生成方法を配置しなければならない.
    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};
        }
    }
    
    function range(start, stop){
        return new RangeIterator(start, stop);
    }
    
    for(let v of range(0,3)) {
        console.log(value);  // 0 1 2
    }
    
    以下は、エルゴードによりポインタ構造を実現する例である.
    function Obj(value){
        this.value = value;
        this.next = null;
    }
    
    Obj.prototype[Symbol.iterator] = function(){
        let iterator = {next : next};  //     
        
        let current = this;
        
        //            next  
        function next(){
            console.log(current);
            if(current){
                let value = current.value;
                current = current.next;
                return {done : false, value : value}  //return     
            }else{
                return {done : true};
            }
        }
        return iterator;
    }
    
    let one = new Obj(1);
    let two = new Obj(2);
    let three = new Obj(3);
    
    one.next = two;
    two.next = three;
    
    for(let i of one){
        console.log(i);
    }
    
    
    //Obj {value: 1, next: Obj}
    //1
    //Obj {value: 2, next: Obj}
    //2
    //Obj {value: 1, next: null}
    //3
    
    上記のコードはまず構造関数のプロトタイプチェーンにSymbol.iterator方法を展開し、この方法を呼び出してエルゴードオブジェクトiteratorに戻り、オブジェクトのnextメソッドを呼び出し、値を返しながら、内部ポインタ(上のcreate)を次の例に向ける.注意:毎回、巡回対象のnextを呼び出す方法です.
    クラスの配列のオブジェクト(数字のキーパッドとlength属性があります)に対して、Iteratorインターフェースを配置します.簡単な方法はSymbol.iteratorメソッドが直接に配列のIteratorインターフェースを参照します.
    let likeArray = {
        0 : 'a',
        1 : 'b',
        2 : 'c',
        length : 3,
        [Symbol.iterator] : Array.prototype[Symbol.iterator]//  [][Symbol.iterator]
    };
    
    for(let item of iterable){
        console.log(item) //'a','b','c'
    }
    
    一般的なオブジェクト配列のSymbol.iteratorメソッドは効果がありません.
    Object.prototype[Symbol.iterator] = () => {
        let index = 0;
        console.log(this === window);  //true
        let keys = Object.keys(this);
        let length = keys.length;
        let self = this;
        return {next : next};
        function next(){
            if(index++ < length){
                return {
                    value : self[keys[index]],
                    done : false,
                };
            }else{
                return {
                    done : true
                };
            }
        }
    }
    
    Iteratorインターフェースを呼び出す場合
  • 構成値
  • let set = new Set().add('a').add('b').add('c');
    
    let [x,y] = set;
    //x ='a' y = 'b'
    
    let [first, ...rest] = set;
    //first = 'a'; rest = ['b','c'];
    
  • 拡張演算子
  • //   
    var str = 'hello';
    [...str] //  ['h','e','l','l','o']
    
    //   
    let arr = ['b', 'c'];
    ['a', ...arr, 'd']
    // ['a', 'b', 'c', 'd']
    
  • yield*
  • yield*の後に付いているのは、巡回可能な構造であり、構造のエルゴードインターフェースを起動する.
    let generator = function* () {
      yield 1;
      yield* [2,3,4];
      yield 5;
    };
    
    var iterator = generator();
    
    iterator.next() // { value: 1, done: false }
    iterator.next() // { value: 2, done: false }
    iterator.next() // { value: 3, done: false }
    iterator.next() // { value: 4, done: false }
    iterator.next() // { value: 5, done: false }
    iterator.next() // { value: undefined, done: true }
    
  • for…of
  • Aray.from()
  • Map()、Set()…
  • for...ofサイクル
    一つのデータ構造はSymbol.iterator属性を展開すれば、iteratorインターフェースを持っていると見なされ、for...ofサイクルでそのメンバーを遍歴することができます.つまり、for…ofサイクル内部で呼び出したのは、データ構造のSymbol.iterator方法です.
    for...ofサイクルが使用できる範囲は、配列、セット、およびMap構造、いくつかの類似配列のオブジェクト(例えば、argmentsオブジェクト、DOM NodeListオブジェクト)、Generatorオブジェクト、および文字列を含む.
    JavaScriptの元のfor…inサイクルは、対象のキー名を取得するだけで、直接キーの値を取得することができません.ES 6はfor...ofサイクルを提供し、キーの値を遍歴することができます.
    var arr = ['a', 'b', 'c', 'd'];
    
    for (let a in arr) {
      console.log(a); // 0 1 2 3
    }
    
    for (let a of arr) {
      console.log(a); // a b c d
    }
    
    for...ofループを通して配列のインデックスを取得する場合は、配列のインスタンスのentriesメソッドとkeysメソッドを利用しても良いです.
    for...ofループは、エルゴードインターフェースを呼び出します.配列のエルゴードインターフェースは、数値インデックスの属性だけを返します.この点はfor…inサイクルとも違います.
    let arr = [3, 5, 7];
    arr.foo = 'hello';
    
    for (let i in arr) {
      console.log(i); // "0", "1", "2", "foo"
    }
    
    for (let i of arr) {
      console.log(i); //  "3", "5", "7"
    }
    
    let map = new Map().set('a', 1).set('b', 2);
    for (let pair of map) {
      console.log(pair);
    }
    // ['a', 1]
    // ['b', 2]
    
    for (let [key, value] of map) {
      console.log(key + ' : ' + value);
    }
    // a : 1
    // b : 2
    
    let arrayLike = { length: 2, 0: 'a', 1: 'b' };
    
    //   
    for (let x of arrayLike) {
      console.log(x);
    }
    
    //   
    for (let x of Array.from(arrayLike)) {
      console.log(x);
    }
    
    普通のオブジェクトに対して
    普通のオブジェクトに対しては、for...of構造は直接使用できません.エラーが発生します.Iteratorインターフェースを配置してから使用しなければなりません.しかし、このような場合、for…inサイクルは依然としてキーの名前を遍歴するために使用されます.
    let es6 = {
      edition: 6,
      committee: "TC39",
      standard: "ECMA-262"
    };
    
    for (let e in es6) {
      console.log(e);
    }
    // edition
    // committee
    // standard
    
    for (let e of es6) {
      console.log(e);
    }
    // TypeError: es6[Symbol.iterator] is not a function
    
    一つの解決方法は、オブジェクトのキー名をObject.keys法を用いて配列を生成し、この配列を巡回することである.
    for (var key of Object.keys(someObject)) {
      console.log(key + ': ' + someObject[key]);
    }
    
    もう一つの方法はGenerator関数を使って物体を再包装することです.
    function* entries(obj) {
      for (let key of Object.keys(obj)) {
        yield [key, obj[key]];
      }
    }
    
    for (let [key, value] of entries(obj)) {
      console.log(key, '->', value);
    }
    // a -> 1
    // b -> 2
    // c -> 3
    
    他のエルゴード文法との比較
    配列はforループとforEachを使用できます.
    しかし、forEachは途中でforEachサイクルを飛び出すことができません.break、returnは効果がありません.for...inは、配列のキー名を巡回してもよい.ただし、キーが数字の文字列は「0」を表します.
    for…inサイクルにはいくつかの欠点があります.
    配列のキー名は数字ですが、for...inループは文字列をキー名の「0」、「1」、「2」などとします.
    for...inサイクルは、数字キーだけでなく、手動で追加された他のキーを遍歴するだけでなく、プロトタイプチェーン上のキーも含みます.
    場合によっては、for…inサイクルは任意の順序でキー名を巡回します.つまり、for…inサイクルは主にオブジェクトを遍歴するために設計されています.遍歴配列には適用されません.
    for...of:
  • はfor...inと同じ簡潔な文法を持っていますが、for...inの欠点はありません.
  • はforEach方法と違って、break、continue、returnと一緒に使用できます.
  • は、すべてのデータ構造を巡回する統一動作インターフェースを提供する.
  • for (var n of fibonacci) {
      if (n > 1000)
        break;
      console.log(n);
    }