New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

迭代器(iterator) #18

Open
sunyongjian opened this Issue Apr 5, 2017 · 2 comments

Comments

Projects
None yet
2 participants
@sunyongjian
Owner

sunyongjian commented Apr 5, 2017

Iterator

背景

生成器概念在Java,Python等语言中都是具备的,ES6也添加到了JavaScript中。Iterator可以使我们不需要初始化集合,以及索引的变量,而是使用迭代器对象的next方法,返回集合的下一项的值,偏向程序化。

ES5中遍历集合通常都是for循环,数组还有forEach方法,对象就是for-in,而迭代器可以统一处理所有集合数据的方法。迭代器是一个接口,只要你这个数据结构暴露了一个iterator的接口,那就可以完成迭代。ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。

ES5中的loop

  • for循环

    var arr = [1, 2, 3];
    for(var i = 0, len = arr.length; i < len; i++) {
      console.log(arr[i]);
    }

    这是标准for循环的写法,字符串也支持,定义一个变量i作为索引,以跟踪访问的位置,len是数组的长度,条件就是i不能超过len。

    这种写法看起来算是比较简单明了的,只不过for里面要写一大坨,而且当出现嵌套的时候,要定义多个变量去维护不同集合的索引,增加复杂度,容易出错。

  • forEach

    forEach是数组内置方法,写起来比较简洁,问题就是不能中断,跳出循环。而这个是我们经常会遇见的,达到某个条件就不需要往后遍历了。

    var arr = [1, 2, 3];
    arr.forEach(item => console.log(item))
    
  • for-in

    常用来遍历对象,可以获得对象的key值,但是只能提取key,value需要我们自己obj[key]的形式去访问。

     var obj = {
       name: 'syj',
       age: 24,
       sex: 'male',
       hobby: 'girl',
     }
       
    for(var key in obj) {
      console.log(key);
    }

什么是迭代器

迭代器是带有特殊接口的对象。含有一个next()方法,调用返回一个包含两个属性的对象,分别是value和done,value表示当前位置的值,done表示是否迭代完,当为true的时候,调用next就无效了。

ES5模拟一个迭代器

function createIterator(ary) {
  var i = 0;
  return {
    next: function() {
      return {
        value: ary[i++],
        done: i > ary.length
      }
    }
  }
}
var iterator = createIterator(['a', 'b', 'c'])
var done = false;

while (!done) {
  var result = iterator.next();
  console.log(result);
  done = result.done;
}
//{ value: 'a', done: false }
//{ value: 'b', done: false }
//{ value: 'c', done: false }
//{ value: undefined, done: true }

createIterator可以看做一个返回迭代器对象的工厂函数,通过while循环调用返回的iterator.next()会得到result,直到done变为true。用ES5写还是比较麻烦的,而且我们这样写并不支持for...of。很快就会看到ES6真正的写法啦。

timg1

迭代器协议(Iteration protocols)

迭代器对象不是新的语法或新的内置对象,而一种协议( 迭代器协议),所有遵守这个协议的对象,都可以称之为迭代器。也就是说我们上面ES5的写法得到的对象遵循迭代器协议,即包含next,调用next返回一个result{value,done}。

可迭代类型

ES6还引入了一个新的Symbol对象,symbol值是唯一的。定义了一个Symbol.iterator属性,只要对象中含有这个属性,就是可迭代的,可用于for...of。在ES6中,所有的集合对象,包括数组,Map和Set,还有字符串都是可迭代的,因为他们都有默认的迭代器。
不了解Symbol的可以移步Symbol对象是什么

  • 尝试使用for...of

    let ary = ['a', 'b', 'c']; //数组
    
    let str = 'str'; //字符串
    
    let set = new Set(); //Set
    set.add('s');
    set.add('s');
    set.add('e');
    set.add('t');
    
    let map = new Map(); //Map
    let o = {};
    map.set('m', 'm');
    map.set(o, 'object');
    
    let obj = {   //对象
      name: 'syj',
      age: 24
    }
    
    function forOf(list) {
      for(var value of list) {
        console.log(value);
      }
    }
    
    forOf(ary); //a b c
    forOf(str); // s t r
    forOf(set); // s e t
    forOf(map); //[ 'm', 'm' ], [ {}, 'object' ]
    forOf(obj); //TypeError: list[Symbol.iterator] is not a function
    

    通过结果可以看出,确实集合类型和字符串都可以用默认的for...of来迭代。但是对象却不可以,内部抛出一个错误,找不到迭代器的接口,证实了上面的言论。

    也许你不了解Set,Map,Set通常是类似于数组的,无重复项的集合,Map是类似于对象的,但是他的key可以是任何类型,增强版的“键值对”的数据结构。详情了解可以移步Set-Map

  • 可以访问默认的iterator

    数组中默认的iterator我们是可以访问的,用法是一样的。

    let ary = ['a', 'b', 'c'];
    let iterator = ary[Symbol.iterator]();
    console.log(iterator.next()); //{ value: 'a', done: false }
    console.log(iterator.next()); //{ value: 'b', done: false }
    console.log(iterator.next()); //{ value: 'c', done: false }
    console.log(iterator.next()); //{ value: undefined, done: true }

使对象可迭代

前面说了只要对象包含[Symbol.iterator]的属性,就可以通过for...of遍历。我们也可以在对象中添加该属性。

 我们用两种方式
 const obj = {
   b: 2
 }
 const a = 'a'
 obj.a = 1;
 Object.defineProperty(obj, Symbol.iterator, {
   enumerable: false,
   writable: false,
   configurable: true,
   value: function () {
     const that = this;
     let index = 0;
     const ks = Object.keys(that);
     return {
       next: function() {
         return {
           value: that[ks[index++]],
           done: (index > ks.length)
         }
       }
     }
   }
 })
 for(const v of obj) {
   console.log(v); //  2 , 1
 }

通过defineProperty向obj对象中添加[Symbol.iterator],我们在对应的value做的就是通过Object.keys取出它的key,然后调用一次next就往后找一位,可以通过next()尝试一下。因为obj有了[Symbol.iterator],for...of可以找到,并且调用。

 const fibonacci = {
   [Symbol.iterator]: function () {
     let [pre, next] = [0, 1];
     return {
       next() {
         [pre, next] = [next, pre + next];
         return {
           value: next,
           done: next > 1000
         }
       }
     }
   }
 }
 
 for(var n of fibonacci) {
   console.log(n)
 }
 // 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610,987

通过直接声明给对象定义一个iterator。这里是iterator经典的用法。当这个数据集合特别大,甚至无限的时候,我们要把它定义好,取出来都是很庞大的操作,这时候iterator的优势就很明显了。只有调用一次next,才返回下一个值,不调用就没有。假如说我们给done没加限制条件,这个迭代器就永远没有done=true,就会永远可以next。为了防止for...of ,调用next的时候,有可能让我的电脑卡死,限制在1000以内。

还有一些值得关注的点

  • 拓展运算符与iterator关系密切

    借用我们fibonacci的例子,只有含有[Symbol.iterator]属性的对象,才可以使用... 展开。

    const fibonacci = {
       a: 'a',
       b: 'b',
    }
    console.log(...fibonacci); //TypeError:
    //(var)[Symbol.iterator] is not a function
    
    fibonacci[Symbol.iterator] = function () {
      let [pre, next] = [0, 1];
      return {
        next() {
          [pre, next] = [next, pre + next];
          return {
            value: next,
            done: next > 1000
          }
        }
      }
    }    
    console.log(...fibonacci);//

    第一次log我们尝试把fibonacci使用... 展开,但是会报错。把这行console注释掉以后,给这个对象添加一个Symbol.iterator属性,再次使用... ,就会得到之前的斐波那契数列了。 由于ES6的数组等集合默认有Symbol.iterator属性,所以我们都是可以直接使用扩展运算符。

  • 类数组

    ES6中对于类数组,也添加了默认迭代器,比如NodeList,它和数组的默认迭代器的用法是一样的。这意味着你可以使用 for...of 循环或在任何对象默认迭代器的内部来迭代 NodeList

    var divs = document.getElementsByTagName("div");
    
    for (const element of divs) {
      console.log(element.id);
    }

总结

ES6提出迭代器的概念,契合了JS语言发展的趋势,统一处理数据结构。迭代器是ES6中很重要的部分,我们仅仅使用是很方便的,但是自定义一些iterator,或者更复杂的方式运行迭代器,还需要我们继续学习。
而定义一个对象的迭代器,又与Symbol对象有关,它采用了Symbol里面的一个默认属性iterator,用来访问对象的迭代器。最后可迭代的数据类型,我们都可以用for...of方法循环遍历,而且集合和字符串内置迭代器,我们可以轻松方便的访问。拓展运算符也是基于iterator的拓展,通过...我们可以把其他数据类型转化为数组,因为...通过执行迭代器,并读取返回的value。

下一篇我们还有未完成的高级迭代器-生成器。

@sunyongjian sunyongjian added the ES6 label Apr 5, 2017

@liuyiliuyi

This comment has been minimized.

liuyiliuyi commented Jun 27, 2017

for in “其他数组等类型都不行。“那一块写错了吧, 数组也是可以for in 的, 可以取到对应的索引。

@sunyongjian

This comment has been minimized.

Owner

sunyongjian commented Jun 28, 2017

@liuyiliuyi 是的,感谢... 已修改。我也不知道我当时为啥写了这句话- -

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment