js迭代器与生成器


前言

es6规范新增了两个高级特性:迭代器和生成器。迭代器的使用极大的简化了数据的操作, ECMAScript 较早的版本中,执行迭代必须使用循环或其他辅助结构。随着代码量增加,代码会变得越发混乱。很多语言都通过原生语言结构解决了这个问题,开发者无须事先知道如何迭代就能实现迭代操作。这个解决方案就是迭代器模式。Python、Java、C++,还有其他很多语言都对这个模式提供了完备的支持。JavaScript 在 ECMAScript 6 以后也支持了迭代器模式。


一、理解迭代

  1. 在 JavaScript 中,计数循环(for循环)就是一种最简单的迭代:
for (let i = 1; i <= 10; i++) { 
console.log(i); 
}
  1. 在迭代器出现之前,我们迭代对象一般是用迭代语句比如for循环,但是for循环有个弊端,它是通过递增索引,通过下标的方式访问数据,那么这在一定程度上限制了遍历的数据结构,我们在使用for循环的时候必须知道该遍历对象可以通过下标的方式访问值,比如数组和类数组(nodeList,arguments…)这种可以。但一般遍历的目的是为了操作里面的数据,它是什么数据解构我并不关心,我只想要里面的数据。而且总用for循环会显得代码臃肿,不利于维护。
  2. ES5 新增了 Array.prototype.forEach()方法,向通用迭代需求迈进了一步(但仍然不够理想): 这个方法解决了单独记录索引和通过数组对象取得值的问题。不过,没有办法标识迭代何时终止。因此这个方法只适用于数组,而且回调结构也比较笨拙。
let collection = ['foo', 'bar', 'baz']; 
collection.forEach((item) => console.log(item));
// foo 
// bar 
// baz

一、迭代器

1.什么是迭代器

  • 迭代器是一种特殊的对象,所有的迭代器对象都有一个next方法,调用该方法会返回一个结果对象,结果对象有两个属性value,done。迭代器对象还会保存一个内部指针,用来指向当前集合中的位置,迭代器每次调用Next方法都会返回一个可用的值。
  • 下面用ES5的语法创建一个迭代器
   let createIterator = function (data) {
            //i是当前集合中的位置
            let i = 0;
            return {
                next: function () {
                    let done = i >= data.length;
                    let value = done ? undefined : data[i++];
                    return {
                        done: done,
                        value: value
                    }
                }
            }
        }

        let iterator = createIterator([1, 2, 3]);
        console.log(iterator.next());//{done: false, value: 1}
        console.log(iterator.next());//{done: false, value: 2}
        console.log(iterator.next());//{done: false, value: 3}
        console.log(iterator.next());//{done: true, value: undefined}
        //后面的都是
        console.log(iterator.next());//{done: true, value: undefined}

2.可迭代协议

-实现 Iterable 接口(可迭代协议)要求同时具备两种能力:支持迭代的自我识别能力和创建实现Iterator 接口的对象的能力。在ECMAScirpt中,这意味着必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的 Symbol.iterator 作为键。这个默认迭代器属性必须引用一个迭代器工厂函数(也可以叫生成器后面会提到),调用这个工厂函数必须返回一个新迭代器。
很多内置类型都实现了 Iterable 接口:

  • 字符串
  • 数组
  • 映射
  • 集合
  • arguments
  • NodeList等DOM集合类型
    检查是否存在默认迭代器属性,可以通过"对象[Symbol.iterator]"的方式
// 这两种类型没有实现迭代器工厂函数
console.log(num[Symbol.iterator]); // undefined 
console.log(obj[Symbol.iterator]); // undefined 
let str = 'abc'; 
let arr = ['a', 'b', 'c']; 
let map = new Map().set('a', 1).set('b', 2).set('c', 3); 
let set = new Set().add('a').add('b').add('c'); 
let els = document.querySelectorAll('div'); 
// 这些类型都实现了迭代器工厂函数
console.log(str[Symbol.iterator]); // f values() { [native code] } 
console.log(arr[Symbol.iterator]); // f values() { [native code] } 
console.log(map[Symbol.iterator]); // f values() { [native code] } 
console.log(set[Symbol.iterator]); // f values() { [native code] } 
console.log(els[Symbol.iterator]); // f values() { [native code] } 
// 调用这个工厂函数会生成一个迭代器
console.log(str[Symbol.iterator]()); // StringIterator {} 
console.log(arr[Symbol.iterator]()); // ArrayIterator {} 
console.log(map[Symbol.iterator]()); // MapIterator {} 
console.log(set[Symbol.iterator]()); // SetIterator {} 
console.log(els[Symbol.iterator]()); // ArrayIterator {}

在实际开发中我们不需要获取集合的迭代器来迭代对象,接收可迭代对象的原生语言特性包括:

  • for-of 循环
  • 数组解构
  • 扩展操作符
  • Array.from()
  • 创建集合
  • 创建映射
  • Promise.all()接收由期约组成的可迭代对象
  • Promise.race()接收由期约组成的可迭代对象
  • yield*操作符,在生成器中使用

二、生成器

  • 生成器是返回是一种返回迭代器的函数,它是函数。
  • 生成器就是es6的Generator
  • 生成器通过function后的*来表示,函数中会用到新的关键字yieId
  • 生成器可以暂停函数执行,对异步编程意义重大
    function* createIterator() {
            yield 1;
            yield 2;
            yield 3;
        }

        let iterator = createIterator();
        console.log(iterator.next());//{done: false, value: 1}
        console.log(iterator.next());//{done: false, value: 2}
        console.log(iterator.next());//{done: false, value: 3}
        console.log(iterator.next());//{done: true,value: undefined}
  • 生成器的一个常用功能是生成状态机
 function* createIterator() {
           while (true) {
               yield A;
               yield B;
               yield C;
           }
       }

       let iterator = createIterator();
       console.log(iterator.next());//{done: false, value: A}
       console.log(iterator.next());//{done: false, value: B}
       console.log(iterator.next());//{done: false, value: C}
       console.log(iterator.next());//{done: false, value: A}
       console.log(iterator.next());//{done: false, value: B}
  • 可以往next方法传值,值为上次yield的返回值,给第一个next传值,第一次调用next()方法时无论传入什么参数都会被丢弃。因为第一次执行Next,没有上一个yield。

        function* createIterator() {
            let one = yield 1;
            let two = yield one + 2;
            let three = yield two + 3;
        }

        let iterator = createIterator();
        console.log(iterator.next(2));//{done: false, value: 1} 传入的参数2被忽略
        console.log(iterator.next(4));//{done: false, value: 6} 传入的值是上一次yield的返回值也就是One等于4
        console.log(iterator.next(8));//{done: false, value: 11} 同理
        console.log(iterator.next());//{done: true, value: undefined}
        console.log(iterator.next());//{done: true, value: undefined}
  • 委托生成器
    在某些时候我们需要将多个迭代器合成一个生成器,可以通过yield*的形式
  function* createIterator1() {
            yield 1;
            yield 2;
            yield 3;
        }

        function* createIterator2() {
            yield 4;
            yield 5;
        }

        function* createIterator() {
            yield* createIterator1();
            yield* createIterator2();
        }

        let iterator = createIterator();
        console.log(iterator.next());//{value: 1, done: false} 
        console.log(iterator.next());//{value: 2, done: false} 
        console.log(iterator.next());//{value: 3, done: false} 
        console.log(iterator.next());//{value: 4, done: false}
        console.log(iterator.next());//{value: 5, done: false}
        console.log(iterator.next());//{value: undefined, done: true}
        console.log(iterator.next());//{value: undefined, done: true}
  • 使用 yield实现递归算法:yield最有用的地方是实现递归操作,此时生成器可以产生自身。看下面的例子:
function* nTimes(n) { 
if (n > 0) { 
yield* nTimes(n - 1); 
yield n - 1; 
} 
} 
for (const x of nTimes(3)) { 
console.log(x); 
} 
// 0 
// 1 
// 2

三、探究

字符串

  • 有没有想过为什么可以用for循环遍历字符串
    在这里插入图片描述
    因为字符串是有下标的,也就是说Js中的字符串跟数组很像,可以通过下标的方式去访问其中的每一个值,而for循环就是通过递增下标,去对应的数据结构通过下标访问其中的值,但这里不推荐用for循环遍历字符串,如图:
    在这里插入图片描述
     由于该生僻字没有识别出来,所以双字节字符被视作两个独立的编码单元,从而最终在A与B之间打印出2个�
     所幸,ES6的目标是全面支持Unicode,并且我们可以通过改变字符串的默认迭代器来解决这个问题,使其操作字符而不是编码单元。现在,修改前一个示例中字符串的默认迭代器,让for-of循环输出正确的内容,因为js中的字符串默认实现了[Symbol.iterator]所以可以使用for of 想使用for of必须实现[Symbol.iterator]
     
    在这里插入图片描述
    前者一阵子改了一个bug也是字符集的事,添加了一个font-family才把生僻字显示出来
  • 为什么通过扩展运算符字符串可直接转为数组?就是因为字符串默认实现了迭代器
    在这里插入图片描述

对象

  • 前面也提到过了,js中的对象是没有默认实现迭代器的,所以不能使用for of循环,那我们手动实现一下迭代器是不是就可以用了呢?答案是是的。
let obj = {
            a: 1,
            b: 2,
            c: 10,
            d: 100,
            [Symbol.iterator]: function* () {
                const arr = Object.keys(obj);
                let len = arr.length;
                let i = 0;
                while (i < len) {
                    yield this[arr[i++]]
                }
            }
        };
        for (let item of obj) {
            console.log(item);
        }

        obj.e = 1000;
        obj.f = 100000;
        for (let item of obj) {
            console.log("第二次",item);
        }

在这里插入图片描述

  • 通过扩展运算符将对象转换成数组
 let obj = {
           a: 1,
           b: 2,
           c: 10,
           d: 100,
           [Symbol.iterator]: function* () {
               const arr = Object.keys(obj);
               let len = arr.length;
               let i = 0;
               while (i < len) {
                   yield this[arr[i++]];
               }
           }
       };
       let arrList = [...obj];
       console.log("arrList", arrList);

在这里插入图片描述

总结

暂时想到这么多,后续可能会补充。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值